summaryrefslogtreecommitdiffstats
path: root/pkg/integration
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2023-07-03 12:57:11 +1000
committerGitHub <noreply@github.com>2023-07-03 12:57:11 +1000
commit1a36cb9f3fbcf07353b8ae73e8b055d2e0e1d794 (patch)
treef486f588c72676322c341cb9aae209a02cbf9c61 /pkg/integration
parent2be4359e87bee6c737e19b02e4c7896fbc2ebf91 (diff)
parent5d982e1d70aa9a07645a5665130f2e499b7b56c8 (diff)
View filtering (#2680)
Diffstat (limited to 'pkg/integration')
-rw-r--r--pkg/integration/components/int_matcher.go12
-rw-r--r--pkg/integration/components/menu_driver.go12
-rw-r--r--pkg/integration/components/view_driver.go55
-rw-r--r--pkg/integration/tests/commit/search.go1
-rw-r--r--pkg/integration/tests/file/discard_all_dir_changes.go117
-rw-r--r--pkg/integration/tests/file/discard_unstaged_dir_changes.go56
-rw-r--r--pkg/integration/tests/file/discard_unstaged_file_changes.go41
-rw-r--r--pkg/integration/tests/filter_and_search/filter_commit_files.go84
-rw-r--r--pkg/integration/tests/filter_and_search/filter_files.go76
-rw-r--r--pkg/integration/tests/filter_and_search/filter_menu.go48
-rw-r--r--pkg/integration/tests/filter_and_search/filter_remote_branches.go59
-rw-r--r--pkg/integration/tests/filter_and_search/nested_filter.go151
-rw-r--r--pkg/integration/tests/filter_and_search/nested_filter_transient.go106
-rw-r--r--pkg/integration/tests/test_list.go10
14 files changed, 808 insertions, 20 deletions
diff --git a/pkg/integration/components/int_matcher.go b/pkg/integration/components/int_matcher.go
index c80a60c85..4cfd0f958 100644
--- a/pkg/integration/components/int_matcher.go
+++ b/pkg/integration/components/int_matcher.go
@@ -10,9 +10,9 @@ type IntMatcher struct {
func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
- name: fmt.Sprintf("equals '%d'", target),
+ 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 value == target, fmt.Sprintf("Expected %d to equal %d", value, target)
},
})
@@ -21,9 +21,9 @@ func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
- name: fmt.Sprintf("greater than '%d'", target),
+ 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 value > target, fmt.Sprintf("Expected %d to greater than %d", value, target)
},
})
@@ -32,9 +32,9 @@ func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
func (self *IntMatcher) LessThan(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
- name: fmt.Sprintf("less than '%d'", target),
+ 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 value < target, fmt.Sprintf("Expected %d to less than %d", value, target)
},
})
diff --git a/pkg/integration/components/menu_driver.go b/pkg/integration/components/menu_driver.go
index ac620f5a4..4e0b0f6da 100644
--- a/pkg/integration/components/menu_driver.go
+++ b/pkg/integration/components/menu_driver.go
@@ -48,6 +48,18 @@ func (self *MenuDriver) TopLines(matchers ...*TextMatcher) *MenuDriver {
return self
}
+func (self *MenuDriver) Filter(text string) *MenuDriver {
+ self.getViewDriver().FilterOrSearch(text)
+
+ return self
+}
+
+func (self *MenuDriver) LineCount(matcher *IntMatcher) *MenuDriver {
+ self.getViewDriver().LineCount(matcher)
+
+ return self
+}
+
func (self *MenuDriver) checkNecessaryChecksCompleted() {
if !self.hasCheckedTitle {
self.t.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().")
diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go
index db7e76134..2c4a23572 100644
--- a/pkg/integration/components/view_driver.go
+++ b/pkg/integration/components/view_driver.go
@@ -66,7 +66,7 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
// If you only care about a subset of lines, use the ContainsLines method instead.
func (self *ViewDriver) Lines(matchers ...*TextMatcher) *ViewDriver {
self.validateMatchersPassed(matchers)
- self.LineCount(len(matchers))
+ self.LineCount(EqualsInt(len(matchers)))
return self.assertLines(0, matchers...)
}
@@ -470,29 +470,56 @@ func (self *ViewDriver) IsEmpty() *ViewDriver {
return self
}
-func (self *ViewDriver) LineCount(expectedCount int) *ViewDriver {
- if expectedCount == 0 {
- return self.IsEmpty()
+func (self *ViewDriver) LineCount(matcher *IntMatcher) *ViewDriver {
+ view := self.getView()
+
+ self.t.assertWithRetries(func() (bool, string) {
+ lineCount := self.getLineCount()
+ ok, _ := matcher.test(lineCount)
+ return ok, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %s, got %d", view.Name(), matcher.name(), lineCount)
+ })
+
+ return self
+}
+
+func (self *ViewDriver) getLineCount() int {
+ // can't rely entirely on view.BufferLines because it returns 1 even if there's nothing in the view
+ if strings.TrimSpace(self.getView().Buffer()) == "" {
+ return 0
}
view := self.getView()
+ return len(view.BufferLines())
+}
+func (self *ViewDriver) IsVisible() *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
- lines := view.BufferLines()
- return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %d, got %d", view.Name(), expectedCount, len(lines))
+ return self.getView().Visible, fmt.Sprintf("%s: Expected view to be visible, but it was not", self.context)
})
+ return self
+}
+
+func (self *ViewDriver) IsInvisible() *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
- lines := view.BufferLines()
+ return !self.getView().Visible, fmt.Sprintf("%s: Expected view to be visible, but it was not", self.context)
+ })
- // if the view has a single blank line (often the case) we want to treat that as having no lines
- if len(lines) == 1 && expectedCount == 1 {
- actual := strings.TrimSpace(view.Buffer())
- return actual != "", fmt.Sprintf("unexpected number of lines in view '%s'. Expected 1, got 0", view.Name())
- }
+ return self
+}
- return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %d, got %d", view.Name(), expectedCount, len(lines))
- })
+// will filter or search depending on whether the view supports filtering/searching
+func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
+ self.IsFocused()
+
+ self.Press(self.t.keys.Universal.StartSearch).
+ Tap(func() {
+ self.t.ExpectSearch().
+ Type(text).
+ Confirm()
+
+ self.t.Views().Search().IsVisible().Content(Contains(fmt.Sprintf("matches for '%s'", text)))
+ })
return self
}
diff --git a/pkg/integration/tests/commit/search.go b/pkg/integration/tests/commit/search.go
index 1d9390dc7..c0a9dfd0b 100644
--- a/pkg/integration/tests/commit/search.go
+++ b/pkg/integration/tests/commit/search.go
@@ -42,6 +42,7 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Universal.StartSearch).
Tap(func() {
t.ExpectSearch().
+ Clear().
Type("o").
Confirm()
diff --git a/pkg/integration/tests/file/discard_all_dir_changes.go b/pkg/integration/tests/file/discard_all_dir_changes.go
new file mode 100644
index 000000000..1032a180a
--- /dev/null
+++ b/pkg/integration/tests/file/discard_all_dir_changes.go
@@ -0,0 +1,117 @@
+package file
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DiscardAllDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Discarding all changes in a directory",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {
+ },
+ SetupRepo: func(shell *Shell) {
+ // typically we would use more bespoke shell methods here, but I struggled to find a way to do that,
+ // and this is copied over from a legacy integration test which did everything in a big shell script
+ // so I'm just copying it across.
+
+ shell.CreateDir("dir")
+
+ // common stuff
+ shell.RunShellCommand(`echo test > dir/both-deleted.txt`)
+ shell.RunShellCommand(`git checkout -b conflict && git add dir/both-deleted.txt`)
+ shell.RunShellCommand(`echo bothmodded > dir/both-modded.txt && git add dir/both-modded.txt`)
+ shell.RunShellCommand(`echo haha > dir/deleted-them.txt && git add dir/deleted-them.txt`)
+ shell.RunShellCommand(`echo haha2 > dir/deleted-us.txt && git add dir/deleted-us.txt`)
+ shell.RunShellCommand(`echo mod > dir/modded.txt && git add dir/modded.txt`)
+ shell.RunShellCommand(`echo mod > dir/modded-staged.txt && git add dir/modded-staged.txt`)
+ shell.RunShellCommand(`echo del > dir/deleted.txt && git add dir/deleted.txt`)
+ shell.RunShellCommand(`echo del > dir/deleted-staged.txt && git add dir/deleted-staged.txt`)
+ shell.RunShellCommand(`echo change-delete > dir/change-delete.txt && git add dir/change-delete.txt`)
+ shell.RunShellCommand(`echo delete-change > dir/delete-change.txt && git add dir/delete-change.txt`)
+ shell.RunShellCommand(`echo double-modded > dir/double-modded.txt && git add dir/double-modded.txt`)
+ shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed.txt && git add dir/renamed.txt`)
+ shell.RunShellCommand(`git commit -m one`)
+
+ // stuff on other branch
+ shell.RunShellCommand(`git branch conflict_second && git mv dir/both-deleted.txt dir/added-them-changed-us.txt`)
+ shell.RunShellCommand(`git commit -m "dir/both-deleted.txt renamed in dir/added-them-changed-us.txt"`)
+ shell.RunShellCommand(`echo blah > dir/both-added.txt && git add dir/both-added.txt`)
+ shell.RunShellCommand(`echo mod1 > dir/both-modded.txt && git add dir/both-modded.txt`)
+ shell.RunShellCommand(`rm dir/deleted-them.txt && git add dir/deleted-them.txt`)
+ shell.RunShellCommand(`echo modded > dir/deleted-us.txt && git add dir/deleted-us.txt`)
+ shell.RunShellCommand(`git commit -m "two"`)
+
+ // stuff on our branch
+ shell.RunShellCommand(`git checkout conflict_second`)
+ shell.RunShellCommand(`git mv dir/both-deleted.txt dir/changed-them-added-us.txt`)
+ shell.RunShellCommand(`git commit -m "both-deleted.txt renamed in dir/changed-them-added-us.txt"`)
+ shell.RunShellCommand(`echo mod2 > dir/both-modded.txt && git add dir/both-modded.txt`)
+ shell.RunShellCommand(`echo blah2 > dir/both-added.txt && git add dir/both-added.txt`)
+ shell.RunShellCommand(`echo modded > dir/deleted-them.txt && git add dir/deleted-them.txt`)
+ shell.RunShellCommand(`rm dir/deleted-us.txt && git add dir/deleted-us.txt`)
+ shell.RunShellCommand(`git commit -m "three"`)
+ shell.RunShellCommand(`git reset --hard conflict_second`)
+ shell.RunCommandExpectError([]string{"git", "merge", "conflict"})
+
+ shell.RunShellCommand(`echo "new" > dir/new.txt`)
+ shell.RunShellCommand(`echo "new staged" > dir/new-staged.txt && git add dir/new-staged.txt`)
+ shell.RunShellCommand(`echo mod2 > dir/modded.txt`)
+ shell.RunShellCommand(`echo mod2 > dir/modded-staged.txt && git add dir/modded-staged.txt`)
+ shell.RunShellCommand(`rm dir/deleted.txt`)
+ shell.RunShellCommand(`rm dir/deleted-staged.txt && git add dir/deleted-staged.txt`)
+ shell.RunShellCommand(`echo change-delete2 > dir/change-delete.txt && git add dir/change-delete.txt`)
+ shell.RunShellCommand(`rm dir/change-delete.txt`)
+ shell.RunShellCommand(`rm dir/delete-change.txt && git add dir/delete-change.txt`)
+ shell.RunShellCommand(`echo "changed" > dir/delete-change.txt`)
+ shell.RunShellCommand(`echo "change1" > dir/double-modded.txt && git add dir/double-modded.txt`)
+ shell.RunShellCommand(`echo "change2" > dir/double-modded.txt`)
+ shell.RunShellCommand(`echo before > dir/added-changed.txt && git add dir/added-changed.txt`)
+ shell.RunShellCommand(`echo after > dir/added-changed.txt`)
+ shell.RunShellCommand(`rm dir/renamed.txt && git add dir/renamed.txt`)
+ shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed2.txt && git add dir/renamed2.txt`)
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Files().
+ IsFocused().
+ Lines(
+ Contains("dir").IsSelected(),
+ Contains("UA").Contains("added-them-changed-us.txt"),
+ Contains("AA").Contains("both-added.txt"),
+ Contains("DD").Contains("both-deleted.txt"),
+ Contains("UU").Contains("both-modded.txt"),
+ Contains("AU").Contains("changed-them-added-us.txt"),
+ Contains("UD").Contains("deleted-them.txt"),
+ Contains("DU").Contains("deleted-us.txt"),
+ ).
+ Press(keys.Universal.Remove).
+ Tap(func() {
+ t.ExpectPopup().Menu().
+ Title(Equals("dir")).
+ Select(Contains("Discard all changes")).
+ Confirm()
+ }).
+ Tap(func() {
+ t.Common().ContinueOnConflictsResolved()
+ }).
+ Lines(
+ Contains("dir").IsSelected(),
+ Contains(" M").Contains("added-changed.txt"),
+ Contains(" D").Contains("change-delete.txt"),
+ Contains("??").Contains("delete-change.txt"),
+ Contains(" D").Contains("deleted.txt"),
+ Contains(" M").Contains("double-modded.txt"),
+ Contains(" M").Contains("modded.txt"),
+ Contains("??").Contains("new.txt"),
+ ).
+ Press(keys.Universal.Remove).
+ Tap(func() {
+ t.ExpectPopup().Menu().
+ Title(Equals("dir")).
+ Select(Contains("Discard all changes")).
+ Confirm()
+ }).
+ IsEmpty()
+ },
+})
diff --git a/pkg/integration/tests/file/discard_unstaged_dir_changes.go b/pkg/integration/tests/file/discard_unstaged_dir_changes.go
new file mode 100644
index 000000000..89e53cab5
--- /dev/null
+++ b/pkg/integration/tests/file/discard_unstaged_dir_changes.go
@@ -0,0 +1,56 @@
+package file
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DiscardUnstagedDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Discarding unstaged changes in a directory",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {
+ },
+ SetupRepo: func(shell *Shell) {
+ shell.CreateDir("dir")
+ shell.CreateFileAndAdd("dir/file-one", "original content\n")
+
+ shell.Commit("first commit")
+
+ shell.UpdateFileAndAdd("dir/file-one", "original content\nnew content\n")
+ shell.UpdateFile("dir/file-one", "original content\nnew content\neven newer content\n")
+
+ shell.CreateDir("dir/subdir")
+ shell.CreateFile("dir/subdir/unstaged-file-one", "unstaged file")
+ shell.CreateFile("dir/unstaged-file-two", "unstaged file")
+
+ shell.CreateFile("unstaged-file-three", "unstaged file")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Files().
+ IsFocused().
+ Lines(
+ Contains("dir").IsSelected(),
+ Contains("subdir"),
+ Contains("??").Contains("unstaged-file-one"),
+ Contains("MM").Contains("file-one"),
+ Contains("??").Contains("unstaged-file-two"),
+ Contains("??").Contains("unstaged-file-three"),
+ ).
+ Press(keys.Universal.Remove).
+ Tap(func() {
+ t.ExpectPopup().Menu().
+ Title(Equals("dir")).
+ Select(Contains("Discard unstaged changes")).
+ Confirm()
+ }).
+ Lines(
+ Contains("dir").IsSelected(),
+ Contains("M ").Contains("file-one"),
+ // this guy remains untouched because it wasn't inside the 'dir' directory
+ Contains("??").Contains("unstaged-file-three"),
+ )
+
+ t.FileSystem().FileContent("dir/file-one", Equals("original content\nnew content\n"))
+ },
+})
diff --git a/pkg/integration/tests/file/discard_unstaged_file_changes.go b/pkg/integration/tests/file/discard_unstaged_file_changes.go
new file mode 100644
index 000000000..caa5ef4ab
--- /dev/null
+++ b/pkg/integration/tests/file/discard_unstaged_file_changes.go
@@ -0,0 +1,41 @@
+package file
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DiscardUnstagedFileChanges = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Discarding unstaged changes in a file",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {
+ },
+ SetupRepo: func(shell *Shell) {
+ shell.CreateFileAndAdd("file-one", "original content\n")
+
+ shell.Commit("first commit")
+
+ shell.UpdateFileAndAdd("file-one", "original content\nnew content\n")
+ shell.UpdateFile("file-one", "original content\nnew content\neven newer content\n")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Files().
+ IsFocused().
+ Lines(
+ Contains("MM").Contains("file-one").IsSelected(),
+ ).
+ Press(keys.Universal.Remove).
+ Tap(func() {
+ t.ExpectPopup().Menu().
+ Title(Equals("file-one")).
+ Select(Contains("Discard unstaged changes")).
+ Confirm()
+ }).
+ Lines(
+ Contains("M ").Contains("file-one").IsSelected(),
+ )
+
+ t.FileSystem().FileContent("file-one", Equals("original content\nnew content\n"))
+ },
+})
diff --git a/pkg/integration/tests/filter_and_search/filter_commit_files.go b/pkg/integration/tests/filter_and_search/filter_commit_files.go
new file mode 100644
index 000000000..953eaf34d
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_commit_files.go
@@ -0,0 +1,84 @@
+package filter_and_search
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterCommitFiles = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Basic commit file filtering by text",
+ ExtraCmdArgs: []string{},
+ Skip: true, // skipping until we have implemented file view filtering
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ shell.CreateDir("folder1")
+ shell.CreateFileAndAdd("folder1/apple-grape", "apple-grape")
+ shell.CreateFileAndAdd("folder1/apple-orange", "apple-orange")
+ shell.CreateFileAndAdd("folder1/grape-orange", "grape-orange")
+ shell.Commit("first commit")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Commits().
+ Focus().
+ Lines(
+ Contains(`first commit`).IsSelected(),
+ ).
+ Press(keys.Universal.Confirm)
+
+ t.Views().CommitFiles().
+ IsFocused().
+ Lines(
+ Contains(`folder1`).IsSelected(),
+ Contains(`apple-grape`),
+ Contains(`apple-orange`),
+ Contains(`grape-orange`),
+ ).
+ Press(keys.Files.ToggleTreeView).
+ Lines(
+ Contains(`folder1/apple-grape`).IsSelected(),
+ Contains(`folder1/apple-orange`),
+ Contains(`folder1/grape-orange`),
+ ).
+ FilterOrSearch("apple").
+ Lines(
+ Contains(`folder1/apple-grape`).IsSelected(),
+ Contains(`folder1/apple-orange`),
+ ).
+ Press(keys.Files.ToggleTreeView).
+ // filter still applies when we toggle tree view
+ Lines(
+ Contains(`folder1`),
+ Contains(`apple-grape`).IsSelected(),
+ Contains(`apple-orange`),
+ ).
+ Press(keys.Files.ToggleTreeView).
+ Lines(
+ Contains(`folder1/apple-grape`).IsSelected(),
+ Contains(`folder1/apple-orange`),
+ ).
+ NavigateToLine(Contains(`folder1/apple-orange`)).
+ Press(keys.Universal.Return).
+ Lines(
+ Contains(`folder1/apple-grape`),
+ // selection is retained after escaping filter mode
+ Contains(`folder1/apple-orange`).IsSelected(),
+ Contains(`folder1/grape-orange`),
+ ).
+ Tap(func() {
+ t.Views().Search().IsInvisible()
+ }).
+ Press(keys.Files.ToggleTreeView).
+ Lines(
+ Contains(`folder1`),
+ Contains(`apple-grape`),
+ Contains(`apple-orange`).IsSelected(),
+ Contains(`grape-orange`),
+ ).
+ FilterOrSearch("folder1/grape").
+ Lines(
+ // first item is always selected after filtering
+ Contains(`folder1`).IsSelected(),
+ Contains(`grape-orange`),
+ )
+ },
+})
diff --git a/pkg/integration/tests/filter_and_search/filter_files.go b/pkg/integration/tests/filter_and_search/filter_files.go
new file mode 100644
index 000000000..6eae90c18
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_files.go
@@ -0,0 +1,76 @@
+package filter_and_search
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterFiles = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Basic file filtering by text",
+ ExtraCmdArgs: []string{},
+ Skip: true, // Skipping until we have implemented file view filtering
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ shell.CreateDir("folder1")
+ shell.CreateFile("folder1/apple-grape", "apple-grape")
+ shell.CreateFile("folder1/apple-orange", "apple-orange")
+ shell.CreateFile("folder1/grape-orange", "grape-orange")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Files().
+ Focus().
+ Lines(
+ Contains(`folder1`).IsSelected(),
+ Contains(`apple-grape`),
+ Contains(`apple-orange`),
+ Contains(`grape-orange`),
+ ).
+ Press(keys.Files.ToggleTreeView).
+ Lines(
+ Contains(`folder1/apple-grape`).IsSelected(),
+ Contains(`folder1/apple-orange`),
+ Contains(`folder1/grape-orange`),
+ ).
+ FilterOrSearch("apple").
+ Lines(
+ Contains(`folder1/apple-grape`).IsSelected(),
+ Contains(`folder1/apple-orange`),
+ ).
+ Press(keys.Files.ToggleTreeView).
+ // filter still applies when we toggle tree view
+ Lines(
+ Contains(`folder1`),
+ Contains(`apple-grape`).IsSelected(),
+ Contains(`apple-orange`),
+ ).
+ Press(keys.Files.ToggleTreeView).
+ Lines(
+ Contains(`folder1/apple-grape`).IsSelected(),
+ Contains(`folder1/apple-orange`),
+ ).
+ NavigateToLine(Contains(`folder1/apple-orange`)).
+ Press(keys.Universal.Return).
+ Lines(
+ Contains(`folder1/apple-grape`),
+ // selection is retained after escaping filter mode
+ Contains(`folder1/apple-orange`).IsSelected(),
+ Contains(`folder1/grape-orange`),
+ ).
+ Tap(func() {
+ t.Views().Search().IsInvisible()
+ }).
+ Press(keys.Files.ToggleTreeView).
+ Lines(
+ Contains(`folder1`),
+ Contains(`apple-grape`),
+ Contains(`apple-orange`).IsSelected(),
+ Contains(`grape-orange`),
+ ).
+ FilterOrSearch("folder1/grape").
+ Lines(
+ // first item is always selected after filtering
+ Contains(`folder1`).IsSelected(),
+ Contains(`grape-orange`),
+ )
+ },
+})
diff --git a/pkg/integration/tests/filter_and_search/filter_menu.go b/pkg/integration/tests/filter_and_search/filter_menu.go
new file mode 100644
index 000000000..5dc15b663
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_menu.go
@@ -0,0 +1,48 @@
+package filter_and_search
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterMenu = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Filtering the keybindings menu",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ shell.CreateFile("myfile", "myfile")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Files().
+ IsFocused().
+ Lines(
+ Contains(`??`).Contains(`myfile`).IsSelected(),
+ ).
+ Press(keys.Universal.OptionMenu).
+ Tap(func() {
+ t.ExpectPopup().Menu().
+ Title(Equals("Keybindings")).
+ Filter("Toggle staged").
+ Lines(
+ // menu has filtered down to the one item that matches the filter
+ Contains(`Toggle staged`).IsSelected(),
+ ).
+ Confirm()
+ })
+
+ t.Views().Files().
+ IsFocused().
+ Lines(
+ // file has been staged
+ Contains(`A `).Contains(`myfile`).IsSelected(),
+ ).
+ // Upon opening the menu again, the filter should have been reset
+ Press(keys.Universal.OptionMenu).
+ Tap(func() {
+ t.ExpectPopup().Menu().
+ Title(Equals("Keybindings")).
+ LineCount(GreaterThan(1))
+ })
+ },
+})
diff --git a/pkg/integration/tests/filter_and_search/filter_remote_branches.go b/pkg/integration/tests/filter_and_search/filter_remote_branches.go
new file mode 100644
index 000000000..11cfea30b
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_remote_branches.go
@@ -0,0 +1,59 @@
+package filter_and_search
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterRemoteBranches = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Filtering remote branches",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ shell.NewBranch("branch-apple")
+ shell.EmptyCommit("commit-one")
+ shell.NewBranch("branch-grape")
+ shell.NewBranch("branch-orange")
+
+ shell.CloneIntoRemote("origin")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Remotes().
+ Focus().
+ Lines(
+ Contains(`origin`).IsSelected(),
+ ).
+ PressEnter()
+
+ t.Views().RemoteBranches().
+ IsFocused().
+ Lines(
+ Contains(`branch-apple`).IsSelected(),
+ Contains(`branch-grape`),
+ Contains(`branch-orange`),
+ ).
+ FilterOrSearch("grape").
+ Lines(
+ Contains(`branch-grape`).IsSelected(),
+ ).
+ // cancel the filter
+ PressEscape().
+ Tap(func() {
+ t.Views().Search().IsInvisible()
+ }).
+ Lines(
+ Contains(`branch-apple`),
+ Contains(`branch-grape`).IsSelected(),
+ Contains(`branch-orange`),
+ ).
+ // return to remotes view
+ PressEscape()
+
+ t.Views().Remotes().
+ IsFocused().
+ Lines(
+ Contains(`origin`).IsSelected(),
+ )
+ },
+})
diff --git a/pkg/integration/tests/filter_and_search/nested_filter.go b/pkg/integration/tests/filter_and_search/nested_filter.go
new file mode 100644
index 000000000..6444ad523
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/nested_filter.go
@@ -0,0 +1,151 @@
+package filter_and_search
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Filter in the several nested panels and verify the filters are preserved as you escape back to the surface",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ // need to create some branches, each with their own commits
+ shell.NewBranch("branch-gold")
+ shell.CreateFileAndAdd("apple", "apple")
+ shell.CreateFileAndAdd("orange", "orange")
+ shell.CreateFileAndAdd("grape", "grape")
+ shell.Commit("commit-knife")
+
+ shell.NewBranch("branch-silver")
+ shell.UpdateFileAndAdd("apple", "apple-2")
+ shell.UpdateFileAndAdd("orange", "orange-2")
+ shell.UpdateFileAndAdd("grape", "grape-2")
+ shell.Commit("commit-spoon")
+
+ shell.NewBranch("branch-bronze")
+ shell.UpdateFileAndAdd("apple", "apple-3")
+ shell.UpdateFileAndAdd("orange", "orange-3")
+ shell.UpdateFileAndAdd("grape", "grape-3")
+ shell.Commit("commit-fork")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Branches().
+ Focus().
+ Lines(
+ Contains(`branch-bronze`).IsSelected(),
+ Contains(`branch-silver`),
+ Contains(`branch-gold`),
+ ).
+ FilterOrSearch("sil").
+ Lines(
+ Contains(`branch-silver`).IsSelected(),
+ ).
+ PressEnter()
+
+ t.Views().SubCommits().
+ IsFocused().
+ Lines(
+ Contains(`commit-spoon`).IsSelected(),
+ Contains(`commit-knife`),
+ ).
+ FilterOrSearch("knife").
+ Lines(
+ // sub-commits view searches, it doesn't filter, so we haven't filtered down the list
+ Contains(`commit-spoon`),
+ Contains(`commit-knife`).IsSelected(),
+ ).
+ PressEnter()
+
+ t.Views().CommitFiles().
+ IsFocused().
+ Lines(
+ Contains(`apple`).IsSelected(),
+ Contains(`grape`),
+ Contains(`orange`),
+ ).
+ FilterOrSearch("grape").
+ Lines(
+ Contains(`apple`),
+ Contains(`grape`).IsSelected(),
+ Contains(`orange`),
+ ).
+ PressEnter()
+
+ t.Views().PatchBuilding().
+ IsFocused().
+ FilterOrSearch("newline").
+ SelectedLine(Contains("No newline at end of file")).
+ PressEscape(). // cancel search
+ Tap(func() {
+ t.Views().Search().IsInvisible()
+ }).
+ // escape to commit-files view
+ PressEscape()
+
+ t.Views().CommitFiles().
+ IsFocused().
+ Lines(
+ Contains(`apple`),
+ Contains(`grape`).IsSelected(),
+ Contains(`orange`),
+ ).
+ Tap(func() {
+ t.Views().Search().IsVisible().Content(Contains("matches for 'grape'"))
+ }).
+ // cancel search
+ PressEscape().
+ Tap(func() {
+ t.Views().Search().IsInvisible()
+ }).
+ Lines(
+ Contains(`apple`),
+ Contains(`grape`).IsSelected(),
+ Contains(`orange`),
+ ).
+ // escape to sub-commits view
+ PressEscape()
+
+ t.Views().SubCommits().
+ IsFocused().
+ Lines(
+ Contains(`commit-spoon`),
+ Contains(`commit-knife`).IsSelected(),
+ ).
+ Tap(func() {
+ t.Views().Search().IsVisible().Content(Contains("matches for 'knife'"))
+ }).
+ // cancel search
+ PressEscape().
+ Tap(func() {
+ t.Views().Search().IsInvisible()
+ }).
+ Lines(
+ Contains(`commit-spoon`),
+ // still selected
+ Contains(`commit-knife`).IsSelected(),
+ ).
+ // escape to branches view
+ PressEscape()
+
+ t.Vi