diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 268d53f1d..cf1e3bbb4 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -49,10 +49,6 @@ func (self *GuiDriver) CurrentContext() types.Context { return self.gui.c.CurrentContext() } -func (self *GuiDriver) Model() *types.Model { - return self.gui.State.Model -} - func (self *GuiDriver) Fail(message string) { self.gui.g.Close() // need to give the gui time to close diff --git a/pkg/integration/README.md b/pkg/integration/README.md index d05e45f0c..616e09e98 100644 --- a/pkg/integration/README.md +++ b/pkg/integration/README.md @@ -24,18 +24,13 @@ In the setup step, we prepare a repo with shell commands, for example, creating ### Run step -The run step has four arguments passed in: +The run step has two arguments passed in: -1. `shell` -2. `input` -3. `assert` -4. `keys` +1. `t` (the test driver) +2. `keys` -`shell` we've already seen in the setup step. The reason it's passed into the run step is that we may want to emulate background events. For example, the user modifying a file outside of lazygit. - -`input` is for driving the gui by pressing certain keys, selecting list items, etc. - -`assert` is for asserting on the state of the lazygit session. When you call a method on `assert`, the assert struct will wait for the assertion to hold true and then continue (failing the test after a timeout). For this reason, assertions have two purposes: one is to ensure the test fails as soon as something unexpected happens, but another is to allow lazygit to process a keypress before you follow up with more keypresses. If you input a bunch of keypresses too quickly lazygit might get confused. +`t` is for driving the gui by pressing certain keys, selecting list items, etc. +`keys` is for use when getting the test to press a particular key e.g. `t.Views().Commits().Focus().PressKey(keys.Universal.Confirm)` ### Tips @@ -43,33 +38,11 @@ The run step has four arguments passed in: Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer. -#### Assert after input - -Use assertions to ensure that lazygit has processed all your keybindings so far. Each time you press a key, something should happen on the screen, so you should assert that that thing has happened. This means we won't get into trouble from keys being entered two quickly because at each stage we ensure the key has been processed. This also makes tests more readable because they help explain what we expect to be happening on-screen. For example: - -```go -input.Press(keys.Files.CommitChanges) -assert.InCommitMessagePanel() -``` - -Note that there are some `input` methods that have assertions baked in, such as the `SwitchToView` methods. - #### Create helper functions for (very) frequently used test logic -If you find yourself doing something frequently in a test, consider making it a method in one of the helper arguments. For example, instead of calling `input.PressKey(keys.Universal.Confirm)` in 100 places, it's better to have a method `input.Confirm()`. This is not to say that everything should be made into a method on the input struct: just things that are particularly common in tests. +If you find yourself doing something frequently in a test, consider making it a method in one of the helper arguments. For example, instead of calling `t.PressKey(keys.Universal.Confirm)` in 100 places, it's better to have a method `t.Confirm()`. This is not to say that everything should be made into a helper method: just things that are particularly common in tests. -Also, given how often we need to select a menu item or type into a prompt panel, there are some helper functions for that. For example: - -```go -// asserts that a prompt opens with the title 'Enter a file name', and then types 'my file' and confirms -input.Prompt(Equals("Enter a file name"), "my file") - -// asserts that a menu opens with the title: 'Choose file content', and then selects the option which contains 'bar' -input.Menu(Equals("Choose file content"), Contains("bar")) - -// asserts a confirmation appears with the title 'Are you sure?' and the content 'Are you REALLY sure' and then confirms -input.AcceptConfirmation(Equals("Are you sure?"), Equals("Are you REALLY sure?")) -``` +Also, given how often we need to select a menu item or type into a prompt panel, there are some helper functions for that. See `ExpectConfirmation` for an example. ## Running tests diff --git a/pkg/integration/components/actions.go b/pkg/integration/components/actions.go new file mode 100644 index 000000000..fb4114890 --- /dev/null +++ b/pkg/integration/components/actions.go @@ -0,0 +1,19 @@ +package components + +// for running common actions +type Actions struct { + t *TestDriver +} + +func (self *Actions) ContinueMerge() { + self.t.Views().current().Press(self.t.keys.Universal.CreateRebaseOptionsMenu) + + self.t.ExpectPopup().Menu(). + Title(Equals("Rebase Options")). + Select(Contains("continue")). + Confirm() +} + +func (self *Actions) ContinueRebase() { + self.ContinueMerge() +} diff --git a/pkg/integration/components/alert_asserter.go b/pkg/integration/components/alert_asserter.go new file mode 100644 index 000000000..2010af3b8 --- /dev/null +++ b/pkg/integration/components/alert_asserter.go @@ -0,0 +1,47 @@ +package components + +type AlertAsserter struct { + t *TestDriver + hasCheckedTitle bool + hasCheckedContent bool +} + +func (self *AlertAsserter) getViewAsserter() *View { + return self.t.Views().Confirmation() +} + +// asserts that the alert view has the expected title +func (self *AlertAsserter) Title(expected *matcher) *AlertAsserter { + self.getViewAsserter().Title(expected) + + self.hasCheckedTitle = true + + return self +} + +// asserts that the alert view has the expected content +func (self *AlertAsserter) Content(expected *matcher) *AlertAsserter { + self.getViewAsserter().Content(expected) + + self.hasCheckedContent = true + + return self +} + +func (self *AlertAsserter) Confirm() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEnter() +} + +func (self *AlertAsserter) Cancel() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEscape() +} + +func (self *AlertAsserter) checkNecessaryChecksCompleted() { + if !self.hasCheckedContent || !self.hasCheckedTitle { + self.t.Fail("You must both check the content and title of a confirmation popup by calling Title()/Content() before calling Confirm()/Cancel().") + } +} diff --git a/pkg/integration/components/assert.go b/pkg/integration/components/assert.go deleted file mode 100644 index ac6b99f0e..000000000 --- a/pkg/integration/components/assert.go +++ /dev/null @@ -1,215 +0,0 @@ -package components - -import ( - "fmt" - "os" - "time" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/types" - integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" -) - -// through this struct we assert on the state of the lazygit gui - -type Assert struct { - gui integrationTypes.GuiDriver -} - -func NewAssert(gui integrationTypes.GuiDriver) *Assert { - return &Assert{gui: gui} -} - -func (self *Assert) WorkingTreeFileCount(expectedCount int) { - self.assertWithRetries(func() (bool, string) { - actualCount := len(self.gui.Model().Files) - - return actualCount == expectedCount, fmt.Sprintf( - "Expected %d changed working tree files, but got %d", - expectedCount, actualCount, - ) - }) -} - -func (self *Assert) CommitCount(expectedCount int) { - self.assertWithRetries(func() (bool, string) { - actualCount := len(self.gui.Model().Commits) - - return actualCount == expectedCount, fmt.Sprintf( - "Expected %d commits present, but got %d", - expectedCount, actualCount, - ) - }) -} - -func (self *Assert) StashCount(expectedCount int) { - self.assertWithRetries(func() (bool, string) { - actualCount := len(self.gui.Model().StashEntries) - - return actualCount == expectedCount, fmt.Sprintf( - "Expected %d stash entries, but got %d", - expectedCount, actualCount, - ) - }) -} - -func (self *Assert) AtLeastOneCommit() { - self.assertWithRetries(func() (bool, string) { - actualCount := len(self.gui.Model().Commits) - - return actualCount > 0, "Expected at least one commit present" - }) -} - -func (self *Assert) HeadCommitMessage(matcher *matcher) { - self.assertWithRetries(func() (bool, string) { - return len(self.gui.Model().Commits) > 0, "Expected at least one commit to be present" - }) - - self.matchString(matcher, "Unexpected commit message.", - func() string { - return self.gui.Model().Commits[0].Name - }, - ) -} - -func (self *Assert) CurrentWindowName(expectedWindowName string) { - self.assertWithRetries(func() (bool, string) { - actual := self.gui.CurrentContext().GetView().Name() - return actual == expectedWindowName, fmt.Sprintf("Expected current window name to be '%s', but got '%s'", expectedWindowName, actual) - }) -} - -func (self *Assert) CurrentBranchName(expectedViewName string) { - self.assertWithRetries(func() (bool, string) { - actual := self.gui.CheckedOutRef().Name - return actual == expectedViewName, fmt.Sprintf("Expected current branch name to be '%s', but got '%s'", expectedViewName, actual) - }) -} - -func (self *Assert) InListContext() { - self.assertWithRetries(func() (bool, string) { - currentContext := self.gui.CurrentContext() - _, ok := currentContext.(types.IListContext) - return ok, fmt.Sprintf("Expected current context to be a list context, but got %s", currentContext.GetKey()) - }) -} - -func (self *Assert) InPrompt() { - self.assertWithRetries(func() (bool, string) { - currentView := self.gui.CurrentContext().GetView() - return currentView.Name() == "confirmation" && currentView.Editable, "Expected prompt popup to be focused" - }) -} - -func (self *Assert) InConfirm() { - self.assertWithRetries(func() (bool, string) { - currentView := self.gui.CurrentContext().GetView() - return currentView.Name() == "confirmation" && !currentView.Editable, "Expected confirmation popup to be focused" - }) -} - -func (self *Assert) InAlert() { - // basically the same thing as a confirmation popup with the current implementation - self.assertWithRetries(func() (bool, string) { - currentView := self.gui.CurrentContext().GetView() - return currentView.Name() == "confirmation" && !currentView.Editable, "Expected alert popup to be focused" - }) -} - -func (self *Assert) InCommitMessagePanel() { - self.assertWithRetries(func() (bool, string) { - currentView := self.gui.CurrentContext().GetView() - return currentView.Name() == "commitMessage", "Expected commit message panel to be focused" - }) -} - -func (self *Assert) InMenu() { - self.assertWithRetries(func() (bool, string) { - return self.gui.CurrentContext().GetView().Name() == "menu", "Expected popup menu to be focused" - }) -} - -func (self *Assert) NotInPopup() { - self.assertWithRetries(func() (bool, string) { - currentViewName := self.gui.CurrentContext().GetView().Name() - return currentViewName != "menu" && currentViewName != "confirmation" && currentViewName != "commitMessage", "Expected popup not to be focused" - }) -} - -func (self *Assert) matchString(matcher *matcher, context string, getValue func() string) { - self.assertWithRetries(func() (bool, string) { - value := getValue() - return matcher.context(context).test(value) - }) -} - -func (self *Assert) assertWithRetries(test func() (bool, string)) { - waitTimes := []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000} - - var message string - for _, waitTime := range waitTimes { - time.Sleep(time.Duration(waitTime) * time.Millisecond) - - var ok bool - ok, message = test() - if ok { - return - } - } - - self.Fail(message) -} - -// for when you just want to fail the test yourself -func (self *Assert) Fail(message string) { - self.gui.Fail(message) -} - -// This does _not_ check the files panel, it actually checks the filesystem -func (self *Assert) FileSystemPathPresent(path string) { - self.assertWithRetries(func() (bool, string) { - _, err := os.Stat(path) - return err == nil, fmt.Sprintf("Expected path '%s' to exist, but it does not", path) - }) -} - -// This does _not_ check the files panel, it actually checks the filesystem -func (self *Assert) FileSystemPathNotPresent(path string) { - self.assertWithRetries(func() (bool, string) { - _, err := os.Stat(path) - return os.IsNotExist(err), fmt.Sprintf("Expected path '%s' to not exist, but it does", path) - }) -} - -func (self *Assert) CurrentView() *ViewAsserter { - return &ViewAsserter{ - context: "current view", - getView: func() *gocui.View { return self.gui.CurrentContext().GetView() }, - assert: self, - } -} - -func (self *Assert) View(viewName string) *ViewAsserter { - return &ViewAsserter{ - context: fmt.Sprintf("%s view", viewName), - getView: func() *gocui.View { return self.gui.View(viewName) }, - assert: self, - } -} - -func (self *Assert) MainView() *ViewAsserter { - return &ViewAsserter{ - context: "main view", - getView: func() *gocui.View { return self.gui.MainView() }, - assert: self, - } -} - -func (self *Assert) SecondaryView() *ViewAsserter { - return &ViewAsserter{ - context: "secondary view", - getView: func() *gocui.View { return self.gui.SecondaryView() }, - assert: self, - } -} diff --git a/pkg/integration/components/assertion_helper.go b/pkg/integration/components/assertion_helper.go new file mode 100644 index 000000000..70f2ff182 --- /dev/null +++ b/pkg/integration/components/assertion_helper.go @@ -0,0 +1,40 @@ +package components + +import ( + "time" + + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" +) + +type assertionHelper struct { + gui integrationTypes.GuiDriver +} + +// milliseconds we'll wait when an assertion fails. +var retryWaitTimes = []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000} + +func (self *assertionHelper) matchString(matcher *matcher, context string, getValue func() string) { + self.assertWithRetries(func() (bool, string) { + value := getValue() + return matcher.context(context).test(value) + }) +} + +func (self *assertionHelper) assertWithRetries(test func() (bool, string)) { + var message string + for _, waitTime := range retryWaitTimes { + time.Sleep(time.Duration(waitTime) * time.Millisecond) + + var ok bool + ok, message = test() + if ok { + return + } + } + + self.fail(message) +} + +func (self *assertionHelper) fail(message string) { + self.gui.Fail(message) +} diff --git a/pkg/integration/components/commit_message_panel_asserter.go b/pkg/integration/components/commit_message_panel_asserter.go new file mode 100644 index 000000000..aad82720f --- /dev/null +++ b/pkg/integration/components/commit_message_panel_asserter.go @@ -0,0 +1,40 @@ +package components + +type CommitMessagePanelAsserter struct { + t *TestDriver +} + +func (self *CommitMessagePanelAsserter) getViewAsserter() *View { + return self.t.Views().CommitMessage() +} + +// asserts on the text initially present in the prompt +func (self *CommitMessagePanelAsserter) InitialText(expected *matcher) *CommitMessagePanelAsserter { + self.getViewAsserter().Content(expected) + + return self +} + +func (self *CommitMessagePanelAsserter) Type(value string) *CommitMessagePanelAsserter { + self.t.typeContent(value) + + return self +} + +func (self *CommitMessagePanelAsserter) AddNewline() *CommitMessagePanelAsserter { + self.t.press(self.t.keys.Universal.AppendNewline) + + return self +} + +func (self *CommitMessagePanelAsserter) Clear() *CommitMessagePanelAsserter { + panic("Clear method not yet implemented!") +} + +func (self *CommitMessagePanelAsserter) Confirm() { + self.getViewAsserter().PressEnter() +} + +func (self *CommitMessagePanelAsserter) Cancel() { + self.getViewAsserter().PressEscape() +} diff --git a/pkg/integration/components/confirmation_asserter.go b/pkg/integration/components/confirmation_asserter.go new file mode 100644 index 000000000..b226f0885 --- /dev/null +++ b/pkg/integration/components/confirmation_asserter.go @@ -0,0 +1,47 @@ +package components + +type ConfirmationAsserter struct { + t *TestDriver + hasCheckedTitle bool + hasCheckedContent bool +} + +func (self *ConfirmationAsserter) getViewAsserter() *View { + return self.t.Views().Confirmation() +} + +// asserts that the confirmation view has the expected title +func (self *ConfirmationAsserter) Title(expected *matcher) *ConfirmationAsserter { + self.getViewAsserter().Title(expected) + + self.hasCheckedTitle = true + + return self +} + +// asserts that the confirmation view has the expected content +func (self *ConfirmationAsserter) Content(expected *matcher) *ConfirmationAsserter { + self.getViewAsserter().Content(expected) + + self.hasCheckedContent = true + + return self +} + +func (self *ConfirmationAsserter) Confirm() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEnter() +} + +func (self *ConfirmationAsserter) Cancel() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEscape() +} + +func (self *ConfirmationAsserter) checkNecessaryChecksCompleted() { + if !self.hasCheckedContent || !self.hasCheckedTitle { + self.t.Fail("You must both check the content and title of a confirmation popup by calling Title()/Content() before calling Confirm()/Cancel().") + } +} diff --git a/pkg/integration/components/file_system.go b/pkg/integration/components/file_system.go new file mode 100644 index 000000000..040234e77 --- /dev/null +++ b/pkg/integration/components/file_system.go @@ -0,0 +1,26 @@ +package components + +import ( + "fmt" + "os" +) + +type FileSystem struct { + *assertionHelper +} + +// This does _not_ check the files panel, it actually checks the filesystem +func (self *FileSystem) PathPresent(path string) { + self.assertWithRetries(func() (bool, string) { + _, err := os.Stat(path) + return err == nil, fmt.Sprintf("Expected path '%s' to exist, but it does not", path) + }) +} + +// This does _not_ check the files panel, it actually checks the filesystem +func (self *FileSystem) PathNotPresent(path string) { + self.assertWithRetries(func() (bool, string) { + _, err := os.Stat(path) + return os.IsNotExist(err), fmt.Sprintf("Expected path '%s' to not exist, but it does", path) + }) +} diff --git a/pkg/integration/components/git.go b/pkg/integration/components/git.go new file mode 100644 index 000000000..b9b3dcc46 --- /dev/null +++ b/pkg/integration/components/git.go @@ -0,0 +1,28 @@ +package components + +import ( + "fmt" + "strings" +) + +type Git struct { + *assertionHelper + shell *Shell +} + +func (self *Git) CurrentBranchName(expectedName string) *Git { + return self.assert("git rev-parse --abbrev-ref HEAD", expectedName) +} + +func (self *Git) assert(cmdStr string, expected string) *Git { + self.assertWithRetries(func() (bool, string) { + output, err := self.shell.runCommandWithOutput(cmdStr) + if err != nil { + return false, fmt.Sprintf("Unexpected error running command: `%s`. Error: %s", cmdStr, err.Error()) + } + actual := strings.TrimSpace(output) + return actual == expected, fmt.Sprintf("Expected current branch name to be '%s', but got '%s'", expected, actual) + }) + + return self +} diff --git a/pkg/integration/components/input.go b/pkg/integration/components/input.go deleted file mode 100644 index 39f1991c1..000000000 --- a/pkg/integration/components/input.go +++ /dev/null @@ -1,263 +0,0 @@ -package components - -import ( - "fmt" - "strings" - "time" - - "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/gui/types" - integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" -) - -type Input struct { - gui integrationTypes.GuiDriver - keys config.KeybindingConfig - assert *Assert - pushKeyDelay int -} - -func NewInput(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, assert *Assert, pushKeyDelay int) *Input { - return &Input{ - gui: gui, - keys: keys, - assert: assert, - pushKeyDelay: pushKeyDelay, - } -} - -// key is something like 'w' or ''. It's best not to pass a direct value, -// but instead to go through the default user config to get a more meaningful key name -func (self *Input) Press(keyStrs ...string) { - for _, keyStr := range keyStrs { - self.press(keyStr) - } -} - -func (self *Input) press(keyStr string) { - self.Wait(self.pushKeyDelay) - - self.gui.PressKey(keyStr) -} - -func (self *Input) SwitchToStatusWindow() { - self.press(self.keys.Universal.JumpToBlock[0]) - self.assert.CurrentWindowName("status") -} - -// switch to status window and assert that the status view is on top -func (self *Input) SwitchToStatusView() { - self.SwitchToStatusWindow() - self.assert.CurrentView().Name("status") -} - -func (self *Input) SwitchToFilesWindow() { - self.press(self.keys.Universal.JumpToBlock[1]) - self.assert.CurrentWindowName("files") -} - -// switch to files window and assert that the files view is on top -func (self *Input) SwitchToFilesView() { - self.SwitchToFilesWindow() - self.assert.CurrentView().Name("files") -} - -func (self *Input) SwitchToBranchesWindow() { - self.press(self.keys.Universal.JumpToBlock[2]) - self.assert.CurrentWindowName("localBranches") -} - -// switch to branches window and assert that the branches view is on top -func (self *Input) SwitchToBranchesView() { - self.SwitchToBranchesWindow() - self.assert.CurrentView().Name("localBranches") -} - -func (self *Input) SwitchToCommitsWindow() { - self.press(self.keys.Universal.JumpToBlock[3]) - self.assert.CurrentWindowName("commits") -} - -// switch to commits window and assert that the commits view is on top -func (self *Input) SwitchToCommitsView() { - self.SwitchToCommitsWindow() - self.assert.CurrentView().Name("commits") -} - -func (self *Input) SwitchToStashWindow() { - self.press(self.keys.Universal.JumpToBlock[4]) - self.assert.CurrentWindowName("stash") -} - -// switch to stash window and assert that the stash view is on top -func (self *Input) SwitchToStashView() { - self.SwitchToStashWindow() - self.assert.CurrentView().Name("stash") -} - -func (self *Input) Type(content string) { - for _, char := range content { - self.press(string(char)) - } -} - -// i.e. pressing enter -func (self *Input) Confirm() { - self.press(self.keys.Universal.Confirm) -} - -// i.e. same as Confirm -func (self *Input) Enter() { - self.press(self.keys.Universal.Confirm) -} - -// i.e. pressing escape -func (self *Input) Cancel() { - self.press(self.keys.Universal.Return) -} - -// i.e. pressing space -func (self *Input) PrimaryAction() { - self.press(self.keys.Universal.Select) -} - -// i.e. pressing down arrow -func (self *Input) NextItem() { - self.press(self.keys.Universal.NextItem) -} - -// i.e. pressing up arrow -func (self *Input) PreviousItem() { - self.press(self.keys.Universal.PrevItem) -} - -func (self *Input) ContinueMerge() { - self.Press(self.keys.Universal.CreateRebaseOptionsMenu) - self.assert.CurrentView().SelectedLine(Contains("continue")) - self.Confirm() -} - -func (self *Input) ContinueRebase() { - self.ContinueMerge() -} - -// for when you want to allow lazygit to process something before continuing -func (self *Input) Wait(milliseconds int) { - time.Sleep(time.Duration(milliseconds) * time.Millisecond) -} - -func (self *Input) LogUI(message string) { - self.gui.LogUI(message) -} - -func (self *Input) Log(message string) { - self.gui.LogUI(message) -} - -// this will look for a list item in the current panel and if it finds it, it will -// enter the keypresses required to navigate to it. -// The test will fail if: -// - the user is not in a list item -// - no list item is found containing the given text -// - multiple list items are found containing the given text in the initial page of items -// -// NOTE: this currently assumes that ViewBufferLines returns all the lines that can be accessed. -// If this changes in future, we'll need to update this code to first attempt to find the item -// in the current page and failing that, jump to the top of the view and iterate through all of it, -// looking for the item. -func (self *Input) NavigateToListItem(matcher *matcher) { - self.assert.InListContext() - - currentContext := self.gui.CurrentContext().(types.IListContext) - - view := currentContext.GetView() - - var matchIndex int - - 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 lines { - ok, _ := matcher.test(line) - if ok { - matches = append(matches, line) - matchIndex = i - } - } - if len(matches) > 1 { - return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n")) - } else if len(matches) == 0 { - return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")) - } else { - return true, "" - } - }) - - selectedLineIdx := view.SelectedLineIdx() - if selectedLineIdx == matchIndex { - self.assert.CurrentView().SelectedLine(matcher) - return - } - if selectedLineIdx < matchIndex { - for i := selectedLineIdx; i < matchIndex; i++ { - self.NextItem() - } - self.assert.CurrentView().SelectedLine(matcher) - return - } else { - for i := selectedLineIdx; i > matchIndex; i-- { - self.PreviousItem() - } - self.assert.CurrentView().SelectedLine(matcher) - return - } -} - -func (self *Input) AcceptConfirmation(title *matcher, content *matcher) { - self.assert.InConfirm() - self.assert.CurrentView().Title(title) - self.assert.CurrentView().Content(content) - self.Confirm() -} - -func (self *Input) DenyConfirmation(title *matcher, content *matcher) { - self.assert.InConfirm() - self.assert.CurrentView().Title(title) - self.assert.CurrentView().Content(content) - self.Cancel() -} - -func (self *Input) Prompt(title *matcher, textToType string) { - self.assert.InPrompt() - self.assert.CurrentView().Title(title) - self.Type(textToType) - self.Confirm() -} - -// type some text into a prompt, then switch to the suggestions panel and expect the first -// item to match the given matcher, then confirm that item. -func (self *Input) Typeahead(title *matcher, textToType string, expectedFirstOption *matcher) { - self.assert.InPrompt() - self.assert.CurrentView().Title(title) - self.Type(textToType) - self.Press(self.keys.Universal.TogglePanel) - self.assert.CurrentView().Name("suggestions") - self.assert.CurrentView().SelectedLine(expectedFirstOption) - self.Confirm() -} - -func (self *Input) Menu(title *matcher, optionToSelect *matcher) { - self.assert.InMenu() - self.assert.CurrentView().Title(title) - self.NavigateToListItem(optionToSelect) - self.Confirm() -} - -func (self *Input) Alert(title *matcher, content *matcher) { - self.assert.InListContext() - self.assert.CurrentView().Title(title) - self.assert.CurrentView().Content(content) - self.Confirm() -} diff --git a/pkg/integration/components/menu_asserter.go b/pkg/integration/components/menu_asserter.go new file mode 100644 index 000000000..42e31a610 --- /dev/null +++ b/pkg/integration/components/menu_asserter.go @@ -0,0 +1,43 @@ +package components + +type MenuAsserter struct { + t *TestDriver + hasCheckedTitle bool +} + +func (self *MenuAsserter) getViewAsserter() *View { + return self.t.Views().Menu() +} + +// asserts that the popup has the expected title +func (self *MenuAsserter) Title(expected *matcher) *MenuAsserter { + self.getViewAsserter().Title(expected) + + self.hasCheckedTitle = true + + return self +} + +func (self *MenuAsserter) Confirm() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEnter() +} + +func (self *MenuAsserter) Cancel() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEscape() +} + +func (self *MenuAsserter) Select(option *matcher) *MenuAsserter { + self.getViewAsserter().NavigateToListItem(option) + + return self +} + +func (self *MenuAsserter) 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/popup.go b/pkg/integration/components/popup.go new file mode 100644 index 000000000..7cd2b2ffc --- /dev/null +++ b/pkg/integration/components/popup.go @@ -0,0 +1,70 @@ +package components + +type Popup struct { + t *TestDriver +} + +func (self *Popup) Confirmation() *ConfirmationAsserter { + self.inConfirm() + + return &ConfirmationAsserter{t: self.t} +} + +func (self *Popup) inConfirm() { + self.t.assertWithRetries(func() (bool, string) { + currentView := self.t.gui.CurrentContext().GetView() + return currentView.Name() == "confirmation" && !currentView.Editable, "Expected confirmation popup to be focused" + }) +} + +func (self *Popup) Prompt() *PromptAsserter { + self.inPrompt() + + return &PromptAsserter{t: self.t} +} + +func (self *Popup) inPrompt() { + self.t.assertWithRetries(func() (bool, string) { + currentView := self.t.gui.CurrentContext().GetView() + return currentView.Name() == "confirmation" && currentView.Editable, "Expected prompt popup to be focused" + }) +} + +func (self *Popup) Alert() *AlertAsserter { + self.inAlert() + + return &AlertAsserter{t: self.t} +} + +func (self *Popup) inAlert() { + // basically the same thing as a confirmation popup with the current implementation + self.t.assertWithRetries(func() (bool, string) { + currentView := self.t.gui.CurrentContext().GetView() + return currentView.Name() == "confirmation" && !currentView.Editable, "Expected alert popup to be focused" + }) +} + +func (self *Popup) Menu() *MenuAsserter { + self.inMenu() + + return &MenuAsserter{t: self.t} +} + +func (self *Popup) inMenu() { + self.t.assertWithRetries(func() (bool, string) { + return self.t.gui.CurrentContext().GetView().Name() == "menu", "Expected popup menu to be focused" + }) +} + +func (self *Popup) CommitMessagePanel() *CommitMessagePanelAsserter { + self.inCommitMessagePanel() + + return &CommitMessagePanelAsserter{t: self.t} +} + +func (self *Popup) inCommitMessagePanel() { + self.t.assertWithRetries(func() (bool, string) { + currentView := self.t.gui.CurrentContext().GetView() + return currentView.Name() == "commitMessage", "Expected commit message panel to be focused" + }) +} diff --git a/pkg/integration/components/prompt_asserter.go b/pkg/integration/components/prompt_asserter.go new file mode 100644 index 000000000..12b19bcd4 --- /dev/null +++ b/pkg/integration/components/prompt_asserter.go @@ -0,0 +1,82 @@ +package components + +type PromptAsserter struct { + t *TestDriver + hasCheckedTitle bool +} + +func (self *PromptAsserter) getViewAsserter() *View { + return self.t.Views().Confirmation() +} + +// asserts that the popup has the expected title +func (self *PromptAsserter) Title(expected *matcher) *PromptAsserter { + self.getViewAsserter().Title(expected) + + self.hasCheckedTitle = true + + return self +} + +// asserts on the text initially present in the prompt +func (self *PromptAsserter) InitialText(expected *matcher) *PromptAsserter { + self.getViewAsserter().Content(expected) + + return self +} + +func (self *PromptAsserter) Type(value string) *PromptAsserter { + self.t.typeContent(value) + + return self +} + +func (self *PromptAsserter) Clear() *PromptAsserter { + panic("Clear method not yet implemented!") +} + +func (self *PromptAsserter) Confirm() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEnter() +} + +func (self *PromptAsserter) Cancel() { + self.checkNecessaryChecksCompleted() + + self.getViewAsserter().PressEscape() +} + +func (self *PromptAsserter) checkNecessaryChecksCompleted() { + if !self.hasCheckedTitle { + self.t.Fail("You must check the title of a prompt popup by calling Title() before calling Confirm()/Cancel().") + } +} + +func (self *PromptAsserter) SuggestionLines(matchers ...*matcher) *PromptAsserter { + self.t.Views().Suggestions().Lines(matchers...) + + return self +} + +func (self *PromptAsserter) SuggestionTopLines(matchers ...*matcher) *PromptAsserter { + self.t.Views().Suggestions().TopLines(matchers...) + + return self +} + +func (self *PromptAsserter) ConfirmFirstSuggestion() { + self.t.press(self.t.keys.Universal.TogglePanel) + self.t.Views().Suggestions(). + IsFocused(). + SelectedLineIdx(0). + PressEnter() +} + +func (self *PromptAsserter) ConfirmSuggestion(matcher *matcher) { + self.t.press(self.t.keys.Universal.TogglePanel) + self.t.Views().Suggestions(). + IsFocused(). + NavigateToListItem(matcher). + PressEnter() +} diff --git a/pkg/integration/components/runner.go b/pkg/integration/components/runner.go index 85ea3f330..964ad98a5 100644 --- a/pkg/integration/components/runner.go +++ b/pkg/integration/components/runner.go @@ -126,7 +126,7 @@ func buildLazygit() error { } func createFixture(test *IntegrationTest, paths Paths) error { - shell := NewShell(paths.ActualRepo()) + shell := NewShell(paths.ActualRepo(), func(errorMsg string) { panic(errorMsg) }) shell.RunCommand("git init -b master") shell.RunCommand(`git config user.email "CI@example.com"`) shell.RunCommand(`git config user.name "CI"`) diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index d0f0345dc..f54d0fa5a 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -15,119 +15,133 @@ import ( type Shell struct { // working directory the shell is invoked in dir string + // when running the shell outside the gui we can directly panic on failure, + // but inside the gui we need to close the gui before panicking + fail func(string) } -func NewShell(dir string) *Shell { - return &Shell{dir: dir} +func NewShell(dir string, fail func(string)) *Shell { + return &Shell{dir: dir, fail: fail} } -func (s *Shell) RunCommand(cmdStr string) *Shell { +func (self *Shell) RunCommand(cmdStr string) *Shell { args := str.ToArgv(cmdStr) cmd := secureexec.Command(args[0], args[1:]...) cmd.Env = os.Environ() - cmd.Dir = s.dir + cmd.Dir = self.dir output, err := cmd.CombinedOutput() if err != nil { - panic(fmt.Sprintf("error running command: %s\n%s", cmdStr, string(output))) + self.fail(fmt.Sprintf("error running command: %s\n%s", cmdStr, string(output))) } - return s + return self } -func (s *Shell) RunShellCommand(cmdStr string) *Shell { +func (self *Shell) runCommandWithOutput(cmdStr string) (string, error) { + args := str.ToArgv(cmdStr) + cmd := secureexec.Command(args[0], args[1:]...) + cmd.Env = os.Environ() + cmd.Dir = self.dir + + output, err := cmd.CombinedOutput() + + return string(output), err +} + +func (self *Shell) RunShellCommand(cmdStr string) *Shell { cmd := secureexec.Command("sh", "-c", cmdStr) cmd.Env = os.Environ() - cmd.Dir = s.dir + cmd.Dir = self.dir output, err := cmd.CombinedOutput() if err != nil { - panic(fmt.Sprintf("error running shell command: %s\n%s", cmdStr, string(output))) + self.fail(fmt.Sprintf("error running shell command: %s\n%s", cmdStr, string(output))) } - return s + return self } -func (s *Shell) RunShellCommandExpectError(cmdStr string) *Shell { +func (self *Shell) RunShellCommandExpectError(cmdStr string) *Shell { cmd := secureexec.Command("sh", "-c", cmdStr) cmd.Env = os.Environ() - cmd.Dir = s.dir + cmd.Dir = self.dir output, err := cmd.CombinedOutput() if err == nil { - panic(fmt.Sprintf("Expected error running shell command: %s\n%s", cmdStr, string(output))) + self.fail(fmt.Sprintf("Expected error running shell command: %s\n%s", cmdStr, string(output))) } - return s + return self } -func (s *Shell) CreateFile(path string, content string) *Shell { - fullPath := filepath.Join(s.dir, path) +func (self *Shell) CreateFile(path string, content string) *Shell { + fullPath := filepath.Join(self.dir, path) err := os.WriteFile(fullPath, []byte(content), 0o644) if err != nil { - panic(fmt.Sprintf("error creating file: %s\n%s", fullPath, err)) + self.fail(fmt.Sprintf("error creating file: %s\n%s", fullPath, err)) } - return s + return self } -func (s *Shell) CreateDir(path string) *Shell { - fullPath := filepath.Join(s.dir, path) +func (self *Shell) CreateDir(path string) *Shell { + fullPath := filepath.Join(self.dir, path) if err := os.MkdirAll(fullPath, 0o755); err != nil { - panic(fmt.Sprintf("error creating directory: %s\n%s", fullPath, err)) + self.fail(fmt.Sprintf("error creating directory: %s\n%s", fullPath, err)) } - return s + return self } -func (s *Shell) UpdateFile(path string, content string) *Shell { - fullPath := filepath.Join(s.dir, path) +func (self *Shell) UpdateFile(path string, content string) *Shell { + fullPath := filepath.Join(self.dir, path) err := os.WriteFile(fullPath, []byte(content), 0o644) if err != nil { - panic(fmt.Sprintf("error updating file: %s\n%s", fullPath, err)) + self.fail(fmt.Sprintf("error updating file: %s\n%s", fullPath, err)) } - return s + return self } -func (s *Shell) NewBranch(name string) *Shell { - return s.RunCommand("git checkout -b " + name) +func (self *Shell) NewBranch(name string) *Shell { + return self.RunCommand("git checkout -b " + name) } -func (s *Shell) Checkout(name string) *Shell { - return s.RunCommand("git checkout " + name) +func (self *Shell) Checkout(name string) *Shell { + return self.RunCommand("git checkout " + name) } -func (s *Shell) Merge(name string) *Shell { - return s.RunCommand("git merge --commit --no-ff " + name) +func (self *Shell) Merge(name string) *Shell { + return self.RunCommand("git merge --commit --no-ff " + name) } -func (s *Shell) GitAdd(path string) *Shell { - return s.RunCommand(fmt.Sprintf("git add \"%s\"", path)) +func (self *Shell) GitAdd(path string) *Shell { + return self.RunCommand(fmt.Sprintf("git add \"%s\"", path)) } -func (s *Shell) GitAddAll() *Shell { - return s.RunCommand("git add -A") +func (self *Shell) GitAddAll() *Shell { + return self.RunCommand("git add -A") } -func (s *Shell) Commit(message string) *Shell { - return s.RunCommand(fmt.Sprintf("git commit -m \"%s\"", message)) +func (self *Shell) Commit(message string) *Shell { + return self.RunCommand(fmt.Sprintf("git commit -m \"%s\"", message)) } -func (s *Shell) EmptyCommit(message string) *Shell { - return s.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message)) +func (self *Shell) EmptyCommit(message string) *Shell { + return self.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message)) } // convenience method for creating a file and adding it -func (s *Shell) CreateFileAndAdd(fileName string, fileContents string) *Shell { - return s. +func (self *Shell) CreateFileAndAdd(fileName string, fileContents string) *Shell { + return self. CreateFile(fileName, fileContents). GitAdd(fileName) } // convenience method for updating a file and adding it -func (s *Shell) UpdateFileAndAdd(fileName string, fileContents string) *Shell { - return s. +func (self *Shell) UpdateFileAndAdd(fileName string, fileContents string) *Shell { + return self. UpdateFile(fileName, fileContents). GitAdd(fileName) } @@ -135,24 +149,24 @@ func (s *Shell) UpdateFileAndAdd(fileName string, fileContents string) *Shell { // creates commits 01, 02, 03, ..., n with a new file in each // The reason for padding with zeroes is so that it's easier to do string // matches on the commit messages when there are many of them -func (s *Shell) CreateNCommits(n int) *Shell { +func (self *Shell) CreateNCommits(n int) *Shell { for i := 1; i <= n; i++ { - s.CreateFileAndAdd( + self.CreateFileAndAdd( fmt.Sprintf("file%02d.txt", i), fmt.Sprintf("file%02d content", i), ). Commit(fmt.Sprintf("commit %02d", i)) } - return s + return self } -func (s *Shell) StashWithMessage(message string) *Shell { - s.RunCommand(fmt.Sprintf(`git stash -m "%s"`, message)) - return s +func (self *Shell) StashWithMessage(message string) *Shell { + self.RunCommand(fmt.Sprintf(`git stash -m "%s"`, message)) + return self } -func (s *Shell) SetConfig(key string, value string) *Shell { - s.RunCommand(fmt.Sprintf(`git config --local "%s" %s`, key, value)) - return s +func (self *Shell) SetConfig(key string, value string) *Shell { + self.RunCommand(fmt.Sprintf(`git config --local "%s" %s`, key, value)) + return self } diff --git a/pkg/integration/components/test.go b/pkg/integration/components/test.go index 46c0d4055..9291cf329 100644 --- a/pkg/integration/components/test.go +++ b/pkg/integration/components/test.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/env" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -24,9 +25,7 @@ type IntegrationTest struct { setupRepo func(shell *Shell) setupConfig func(config *config.AppConfig) run func( - shell *Shell, - input *Input, - assert *Assert, + testDriver *TestDriver, keys config.KeybindingConfig, ) } @@ -41,7 +40,7 @@ type NewIntegrationTestArgs struct { // takes a config and mutates. The mutated context will end up being passed to the gui SetupConfig func(config *config.AppConfig) // runs the test - Run func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) + Run func(t *TestDriver, keys config.KeybindingConfig) // additional args passed to lazygit ExtraCmdArgs string // for when a test is flakey @@ -91,18 +90,20 @@ func (self *IntegrationTest) SetupRepo(shell *Shell) { self.setupRepo(shell) } -// I want access to all contexts, the model, the ability to press a key, the ability to log, func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) { - shell := NewShell("/tmp/lazygit-test") - assert := NewAssert(gui) - keys := gui.Keys() - input := NewInput(gui, keys, assert, KeyPressDelay()) + // we pass the --pass arg to lazygit when running an integration test, and that + // ends up stored in the following env var + repoPath := env.GetGitWorkTreeEnv() - self.run(shell, input, assert, keys) + shell := NewShell(repoPath, func(errorMsg string) { gui.Fail(errorMsg) }) + keys := gui.Keys() + testDriver := NewTestDriver(gui, shell, keys, KeyPressDelay()) + + self.run(testDriver, keys) if KeyPressDelay() > 0 { // the dev would want to see the final state if they're running in slow mode - input.Wait(2000) + testDriver.Wait(2000) } } diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go new file mode 100644 index 000000000..5f2e15435 --- /dev/null +++ b/pkg/integration/components/test_driver.go @@ -0,0 +1,160 @@ +package components + +import ( + "fmt" + "strings" + "time" + + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/types" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" +) + +type TestDriver struct { + gui integrationTypes.GuiDriver + keys config.KeybindingConfig + pushKeyDelay int + *assertionHelper + shell *Shell +} + +func NewTestDriver(gui integrationTypes.GuiDriver, shell *Shell, keys config.KeybindingConfig, pushKeyDelay int) *TestDriver { + return &TestDriver{ + gui: gui, + keys: keys, + pushKeyDelay: pushKeyDelay, + assertionHelper: &assertionHelper{gui: gui}, + shell: shell, + } +} + +// key is something like 'w' or ''. It's best not to pass a direct value, +// but instead to go through the default user config to get a more meaningful key name +func (self *TestDriver) press(keyStr string) { + self.Wait(self.pushKeyDelay) + + self.gui.PressKey(keyStr) +} + +func (self *TestDriver) typeContent(content string) { + for _, char := range content { + self.press(string(char)) + } +} + +func (self *TestDriver) Actions() *Actions { + return &Actions{t: self} +} + +// for when you want to allow lazygit to process something before continuing +func (self *TestDriver) Wait(milliseconds int) { + time.Sleep(time.Duration(milliseconds) * time.Millisecond) +} + +func (self *TestDriver) LogUI(message string) { + self.gui.LogUI(message) +} + +func (self *TestDriver) Log(message string) { + self.gui.LogUI(message) +} + +// allows the user to run shell commands during the test to emulate background activity +func (self *TestDriver) Shell() *Shell { + return self.shell +} + +// this will look for a list item in the current panel and if it finds it, it will +// enter the keypresses required to navigate to it. +// The test will fail if: +// - the user is not in a list item +// - no list item is found containing the given text +// - multiple list items are found containing the given text in the initial page of items +// +// NOTE: this currently assumes that ViewBufferLines returns all the lines that can be accessed. +// If this changes in future, we'll need to update this code to first attempt to find the item +// in the current page and failing that, jump to the top of the view and iterate through all of it, +// looking for the item. +func (self *TestDriver) navigateToListItem(matcher *matcher) { + self.inListContext() + + currentContext := self.gui.CurrentContext().(types.IListContext) + + view := currentContext.GetView() + + var matchIndex int + + self.assertWithRetries(func() (bool, string) { + matchIndex = -1 + var matches []string + lines := view.ViewBufferLines() + // first we look for a duplicate on the current screen. We won't bother looking beyond that though. + for i, line := range lines { + ok, _ := matcher.test(line) + if ok { + matches = append(matches, line) + matchIndex = i + } + } + if len(matches) > 1 { + return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n")) + } else if len(matches) == 0 { + return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")) + } else { + return true, "" + } + }) + + selectedLineIdx := view.SelectedLineIdx() + if selectedLineIdx == matchIndex { + self.Views().current().SelectedLine(matcher) + return + } + if selectedLineIdx < matchIndex { + for i := selectedLineIdx; i < matchIndex; i++ { + self.Views().current().SelectNextItem() + } + self.Views().current().SelectedLine(matcher) + return + } else { + for i := selectedLineIdx; i > matchIndex; i-- { + self.Views().current().SelectPreviousItem() + } + self.Views().current().SelectedLine(matcher) + return + } +} + +func (self *TestDriver) inListContext() { + self.assertWithRetries(func() (bool, string) { + currentContext := self.gui.CurrentContext() + _, ok := currentContext.(types.IListContext) + return ok, fmt.Sprintf("Expected current context to be a list context, but got %s", currentContext.GetKey()) + }) +} + +// for making assertions on lazygit views +func (self *TestDriver) Views() *Views { + return &Views{t: self} +} + +// for interacting with popups +func (self *TestDriver) ExpectPopup() *Popup { + return &Popup{t: self} +} + +// for making assertions through git itself +func (self *TestDriver) Git() *Git { + return &Git{assertionHelper: self.assertionHelper, shell: self.shell} +} + +// for making assertions on the file system +func (self *TestDriver) FileSystem() *FileSystem { + return &FileSystem{assertionHelper: self.assertionHelper} +} + +// for when you just want to fail the test yourself. +// This runs callbacks to ensure we render the error after closing the gui. +func (self *TestDriver) Fail(message string) { + self.assertionHelper.fail(message) +} diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index dbab22883..08823f525 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -30,10 +30,6 @@ func (self *fakeGuiDriver) CurrentContext() types.Context { return nil } -func (self *fakeGuiDriver) Model() *types.Model { - return &types.Model{Commits: []*models.Commit{}} -} - func (self *fakeGuiDriver) Fail(message string) { self.failureMessage = message } @@ -60,26 +56,11 @@ func (self *fakeGuiDriver) View(viewName string) *gocui.View { return nil } -func TestAssertionFailure(t *testing.T) { - test := NewIntegrationTest(NewIntegrationTestArgs{ - Description: unitTestDescription, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.Press("a") - input.Press("b") - assert.CommitCount(2) - }, - }) - driver := &fakeGuiDriver{} - test.Run(driver) - assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys) - assert.Equal(t, "Expected 2 commits present, but got 0", driver.failureMessage) -} - func TestManualFailure(t *testing.T) { test := NewIntegrationTest(NewIntegrationTestArgs{ Description: unitTestDescription, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.Fail("blah") + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Fail("blah") }, }) driver := &fakeGuiDriver{} @@ -90,10 +71,9 @@ func TestManualFailure(t *testing.T) { func TestSuccess(t *testing.T) { test := NewIntegrationTest(NewIntegrationTestArgs{ Description: unitTestDescription, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.Press("a") - input.Press("b") - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.press("a") + t.press("b") }, }) driver := &fakeGuiDriver{} diff --git a/pkg/integration/components/view.go b/pkg/integration/components/view.go new file mode 100644 index 000000000..6386ebf3f --- /dev/null +++ b/pkg/integration/components/view.go @@ -0,0 +1,230 @@ +package components + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/gocui" +) + +type View struct { + // context is prepended to any error messages e.g. 'context: "current view"' + context string + getView func() *gocui.View + t *TestDriver +} + +// asserts that the view has the expected title +func (self *View) Title(expected *matcher) *View { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().Title + return expected.context(fmt.Sprintf("%s title", self.context)).test(actual) + }) + + return self +} + +// asserts that the view has lines matching the given matchers. So if three matchers +// are passed, we only check the first three lines of the view. +// This method is convenient when you have a list of commits but you only want to +// assert on the first couple of commits. +func (self *View) TopLines(matchers ...*matcher) *View { + if len(matchers) < 1 { + self.t.fail("TopLines method requires at least one matcher. If you are trying to assert that there are no lines, use .IsEmpty()") + } + + self.t.assertWithRetries(func() (bool, string) { + lines := self.getView().BufferLines() + return len(lines) >= len(matchers), fmt.Sprintf("unexpected number of lines in view. Expected at least %d, got %d", len(matchers), len(lines)) + }) + + return self.assertLines(matchers...) +} + +// asserts that the view has lines matching the given matchers. One matcher must be passed for each line. +// If you only care about the top n lines, use the TopLines method instead. +func (self *View) Lines(matchers ...*matcher) *View { + self.LineCount(len(matchers)) + + return self.assertLines(matchers...) +} + +func (self *View) assertLines(matchers ...*matcher) *View { + view := self.getView() + + for i, matcher := range matchers { + checkIsSelected, matcher := matcher.checkIsSelected() + + self.t.matchString(matcher, fmt.Sprintf("Unexpected content in view '%s'.", view.Name()), + func() string { + return view.BufferLines()[i] + }, + ) + + if checkIsSelected { + self.t.assertWithRetries(func() (bool, string) { + lineIdx := view.SelectedLineIdx() + return lineIdx == i, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), i, lineIdx) + }) + } + } + + return self +} + +// asserts on the content of the view i.e. the stuff within the view's frame. +func (self *View) Content(matcher *matcher) *View { + self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context), + func() string { + return self.getView().Buffer() + }, + ) + + return self +} + +// asserts on the selected line of the view +func (self *View) SelectedLine(matcher *matcher) *View { + self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected selected line.", self.context), + func() string { + return self.getView().SelectedLine() + }, + ) + + return self +} + +// asserts on the index of the selected line. 0 is the first index, representing the line at the top of the view. +func (self *View) SelectedLineIdx(expected int) *View { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().SelectedLineIdx() + return expected == actual, fmt.Sprintf("%s: Expected selected line index to be %d, got %d", self.context, expected, actual) + }) + + return self +} + +// focus the view (assumes the view is a side-view that can be focused via a keybinding) +func (self *View) Focus() *View { + // we can easily change focus by switching to the view's window, but this assumes that the desired view + // is at the top of that window. So for now we'll switch to the window then assert that the desired + // view is on top (i.e. that it's the current view). + // If we want to support other views e.g. the tags view, we'll need to add more logic here. + viewName := self.getView().Name() + + // using a map rather than a slice because we might add other views which share a window index later + windowIndexMap := map[string]int{ + "status": 0, + "files": 1, + "localBranches": 2, + "commits": 3, + "stash": 4, + } + + index, ok := windowIndexMap[viewName] + if !ok { + self.t.fail(fmt.Sprintf("Cannot focus view %s: Focus() method not implemented", viewName)) + } + + self.t.press(self.t.keys.Universal.JumpToBlock[index]) + + // assert that we land in the expected view + self.IsFocused() + + return self +} + +// asserts that the view is focused +func (self *View) IsFocused() *View { + self.t.assertWithRetries(func() (bool, string) { + expected := self.getView().Name() + actual := self.t.gui.CurrentContext().GetView().Name() + return actual == expected, fmt.Sprintf("%s: Unexpected view focused. Expected %s, got %s", self.context, expected, actual) + }) + + return self +} + +func (self *View) Press(keyStr string) *View { + self.IsFocused() + + self.t.press(keyStr) + + return self +} + +// i.e. pressing down arrow +func (self *View) SelectNextItem() *View { + return self.Press(self.t.keys.Universal.NextItem) +} + +// i.e. pressing up arrow +func (self *View) SelectPreviousItem() *View { + return self.Press(self.t.keys.Universal.PrevItem) +} + +// i.e. pressing space +func (self *View) PressPrimaryAction() *View { + return self.Press(self.t.keys.Universal.Select) +} + +// i.e. pressing space +func (self *View) PressEnter() *View { + return self.Press(self.t.keys.Universal.Confirm) +} + +// i.e. pressing escape +func (self *View) PressEscape() *View { + return self.Press(self.t.keys.Universal.Return) +} + +func (self *View) NavigateToListItem(matcher *matcher) *View { + self.IsFocused() + + self.t.navigateToListItem(matcher) + + return self +} + +// returns true if the view is a list view and it contains no items +func (self *View) IsEmpty() *View { + self.t.assertWithRetries(func() (bool, string) { + actual := strings.TrimSpace(self.getView().Buffer()) + return actual == "", fmt.Sprintf("%s: Unexpected content in view: expected no content. Content: %s", self.context, actual) + }) + + return self +} + +func (self *View) LineCount(expectedCount int) *View { + if expectedCount == 0 { + return self.IsEmpty() + } + + self.t.assertWithRetries(func() (bool, string) { + lines := self.getView().BufferLines() + return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view. Expected %d, got %d", expectedCount, len(lines)) + }) + + self.t.assertWithRetries(func() (bool, string) { + lines := self.getView().BufferLines() + + // 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(self.getView().Buffer()) + return actual != "", "unexpected number of lines in view. Expected 1, got 0" + } + + return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view. Expected %d, got %d", expectedCount, len(lines)) + }) + + return self +} + +// for when you want to make some assertion unrelated to the current view +// without breaking the method chain +func (self *View) Tap(f func()) *View { + f() + + return self +} diff --git a/pkg/integration/components/view_asserter.go b/pkg/integration/components/view_asserter.go deleted file mode 100644 index 73f315993..000000000 --- a/pkg/integration/components/view_asserter.go +++ /dev/null @@ -1,114 +0,0 @@ -package components - -import ( - "fmt" - - "github.com/jesseduffield/gocui" -) - -type ViewAsserter struct { - // context is prepended to any error messages e.g. 'context: "current view"' - context string - getView func() *gocui.View - assert *Assert -} - -// asserts that the view has the expected name. This is typically used in tandem with the CurrentView method i.e.; -// assert.CurrentView().Name("commits") to assert that the current view is the commits view. -func (self *ViewAsserter) Name(expected string) *ViewAsserter { - self.assert.assertWithRetries(func() (bool, string) { - actual := self.getView().Name() - return actual == expected, fmt.Sprintf("%s: Expected view name to be '%s', but got '%s'", self.context, expected, actual) - }) - - return self -} - -// asserts that the view has the expected title -func (self *ViewAsserter) Title(expected *matcher) *ViewAsserter { - self.assert.assertWithRetries(func() (bool, string) { - actual := self.getView().Title - return expected.context(fmt.Sprintf("%s title", self.context)).test(actual) - }) - - return self -} - -// asserts that the view has lines matching the given matchers. So if three matchers -// are passed, we only check the first three lines of the view. -// This method is convenient when you have a list of commits but you only want to -// assert on the first couple of commits. -func (self *ViewAsserter) TopLines(matchers ...*matcher) *ViewAsserter { - self.assert.assertWithRetries(func() (bool, string) { - lines := self.getView().BufferLines() - return len(lines) >= len(matchers), fmt.Sprintf("unexpected number of lines in view. Expected at least %d, got %d", len(matchers), len(lines)) - }) - - return self.assertLines(matchers...) -} - -// asserts that the view has lines matching the given matchers. One matcher must be passed for each line. -// If you only care about the top n lines, use the TopLines method instead. -func (self *ViewAsserter) Lines(matchers ...*matcher) *ViewAsserter { - self.assert.assertWithRetries(func() (bool, string) { - lines := self.getView().BufferLines() - return len(lines) == len(matchers), fmt.Sprintf("unexpected number of lines in view. Expected %d, got %d", len(matchers), len(lines)) - }) - - return self.assertLines(matchers...) -} - -func (self *ViewAsserter) assertLines(matchers ...*matcher) *ViewAsserter { - view := self.getView() - - for i, matcher := range matchers { - checkIsSelected, matcher := matcher.checkIsSelected() - - self.assert.matchString(matcher, fmt.Sprintf("Unexpected content in view '%s'.", view.Name()), - func() string { - return view.BufferLines()[i] - }, - ) - - if checkIsSelected { - self.assert.assertWithRetries(func() (bool, string) { - lineIdx := view.SelectedLineIdx() - return lineIdx == i, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), i, lineIdx) - }) - } - } - - return self -} - -// asserts on the content of the view i.e. the stuff within the view's frame. -func (self *ViewAsserter) Content(matcher *matcher) *ViewAsserter { - self.assert.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context), - func() string { - return self.getView().Buffer() - }, - ) - - return self -} - -// asserts on the selected line of the view -func (self *ViewAsserter) SelectedLine(matcher *matcher) *ViewAsserter { - self.assert.matchString(matcher, fmt.Sprintf("%s: Unexpected selected line.", self.context), - func() string { - return self.getView().SelectedLine() - }, - ) - - return self -} - -// asserts on the index of the selected line. 0 is the first index, representing the line at the top of the view. -func (self *ViewAsserter) SelectedLineIdx(expected int) *ViewAsserter { - self.assert.assertWithRetries(func() (bool, string) { - actual := self.getView().SelectedLineIdx() - return expected == actual, fmt.Sprintf("%s: Expected selected line index to be %d, got %d", self.context, expected, actual) - }) - - return self -} diff --git a/pkg/integration/components/views.go b/pkg/integration/components/views.go new file mode 100644 index 000000000..4d4348873 --- /dev/null +++ b/pkg/integration/components/views.go @@ -0,0 +1,121 @@ +package components + +import ( + "fmt" + + "github.com/jesseduffield/gocui" +) + +type Views struct { + t *TestDriver +} + +// not exporting this because I want the test to always be explicit about what +// view it's dealing with. +func (self *Views) current() *View { + return &View{ + context: "current view", + getView: func() *gocui.View { return self.t.gui.CurrentContext().GetView() }, + t: self.t, + } +} + +func (self *Views) Main() *View { + return &View{ + context: "main view", + getView: func() *gocui.View { return self.t.gui.MainView() }, + t: self.t, + } +} + +func (self *Views) Secondary() *View { + return &View{ + context: "secondary view", + getView: func() *gocui.View { return self.t.gui.SecondaryView() }, + t: self.t, + } +} + +func (self *Views) byName(viewName string) *View { + return &View{ + context: fmt.Sprintf("%s view", viewName), + getView: func() *gocui.View { return self.t.gui.View(viewName) }, + t: self.t, + } +} + +func (self *Views) Commits() *View { + return self.byName("commits") +} + +func (self *Views) Files() *View { + return self.byName("files") +} + +func (self *Views) Status() *View { + return self.byName("status") +} + +func (self *Views) Submodules() *View { + return self.byName("submodules") +} + +func (self *Views) Information() *View { + return self.byName("information") +} + +func (self *Views) Branches() *View { + return self.byName("localBranches") +} + +func (self *Views) RemoteBranches() *View { + return self.byName("remoteBranches") +} + +func (self *Views) Tags() *View { + return self.byName("tags") +} + +func (self *Views) ReflogCommits() *View { + return self.byName("reflogCommits") +} + +func (self *Views) SubCommits() *View { + return self.byName("subCommits") +} + +func (self *Views) CommitFiles() *View { + return self.byName("commitFiles") +} + +func (self *Views) Stash() *View { + return self.byName("stash") +} + +func (self *Views) Staging() *View { + return self.byName("staging") +} + +func (self *Views) StagingSecondary() *View { + return self.byName("stagingSecondary") +} + +func (self *Views) Menu() *View { + return self.byName("menu") +} + +func (self *Views) Confirmation() *View { + return self.byName("confirmation") +} + +func (self *Views) CommitMessage() *View { + return self.byName("commitMessage") +} + +func (self *Views) Suggestions() *View { + return self.byName("suggestions") +} + +func (self *Views) MergeConflicts() *View { + return self.byName("mergeConflicts") +} diff --git a/pkg/integration/tests/bisect/basic.go b/pkg/integration/tests/bisect/basic.go index 45d66fa18..ca06d5fa5 100644 --- a/pkg/integration/tests/bisect/basic.go +++ b/pkg/integration/tests/bisect/basic.go @@ -14,57 +14,46 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ CreateNCommits(10) }, SetupConfig: func(cfg *config.AppConfig) {}, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { + Run: func(t *TestDriver, keys config.KeybindingConfig) { markCommitAsBad := func() { - input.Press(keys.Commits.ViewBisectOptions) - input.Menu(Equals("Bisect"), MatchesRegexp(`mark .* as bad`)) + t.Views().Commits(). + Press(keys.Commits.ViewBisectOptions) + + t.ExpectPopup().Menu().Title(Equals("Bisect")).Select(MatchesRegexp(`mark .* as bad`)).Confirm() } markCommitAsGood := func() { - input.Press(keys.Commits.ViewBisectOptions) - input.Menu(Equals("Bisect"), MatchesRegexp(`mark .* as good`)) + t.Views().Commits(). + Press(keys.Commits.ViewBisectOptions) + + t.ExpectPopup().Menu().Title(Equals("Bisect")).Select(MatchesRegexp(`mark .* as good`)).Confirm() } - assert.AtLeastOneCommit() + t.Views().Commits(). + Focus(). + SelectedLine(Contains("commit 10")). + NavigateToListItem(Contains("commit 09")). + Tap(func() { + markCommitAsBad() - input.SwitchToCommitsView() + t.Views().Information().Content(Contains("bisecting")) + }). + SelectedLine(Contains("<-- bad")). + NavigateToListItem(Contains("commit 02")). + Tap(markCommitAsGood). + // lazygit will land us in the commit between our good and bad commits. + SelectedLine(Contains("commit 05").Contains("<-- current")). + Tap(markCommitAsBad). + SelectedLine(Contains("commit 04").Contains("<-- current")). + Tap(func() { + markCommitAsGood() - assert.CurrentView().SelectedLine(Contains("commit 10")) + // commit 5 is the culprit because we marked 4 as good and 5 as bad. + t.ExpectPopup().Alert().Title(Equals("Bisect complete")).Content(MatchesRegexp("(?s)commit 05.*Do you want to reset")).Confirm() + }). + IsFocused(). + Content(Contains("commit 04")) - input.NavigateToListItem(Contains("commit 09")) - - markCommitAsBad() - - assert.View("information").Content(Contains("bisecting")) - - assert.CurrentView().Name("commits").SelectedLine(Contains("<-- bad")) - - input.NavigateToListItem(Contains("commit 02")) - - markCommitAsGood() - - // lazygit will land us in the commit between our good and bad commits. - assert.CurrentView(). - Name("commits"). - SelectedLine(Contains("commit 05").Contains("<-- current")) - - markCommitAsBad() - - assert.CurrentView(). - Name("commits"). - SelectedLine(Contains("commit 04").Contains("<-- current")) - - markCommitAsGood() - - // commit 5 is the culprit because we marked 4 as good and 5 as bad. - 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(DoesNotContain("bisecting")) + t.Views().Information().Content(DoesNotContain("bisecting")) }, }) diff --git a/pkg/integration/tests/bisect/from_other_branch.go b/pkg/integration/tests/bisect/from_other_branch.go index cb31af972..ecac6ea3f 100644 --- a/pkg/integration/tests/bisect/from_other_branch.go +++ b/pkg/integration/tests/bisect/from_other_branch.go @@ -18,37 +18,29 @@ var FromOtherBranch = NewIntegrationTest(NewIntegrationTestArgs{ RunCommand("git bisect start other~2 other~5") }, SetupConfig: func(cfg *config.AppConfig) {}, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { - assert.View("information").Content(Contains("bisecting")) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Information().Content(Contains("bisecting")) - assert.AtLeastOneCommit() + t.Views().Commits(). + Focus(). + TopLines( + MatchesRegexp(`<-- bad.*commit 08`), + MatchesRegexp(`<-- current.*commit 07`), + MatchesRegexp(`\?.*commit 06`), + MatchesRegexp(`<-- good.*commit 05`), + ). + SelectNextItem(). + Press(keys.Commits.ViewBisectOptions). + Tap(func() { + t.ExpectPopup().Menu().Title(Equals("Bisect")).Select(MatchesRegexp(`mark .* as good`)).Confirm() - input.SwitchToCommitsView() + t.ExpectPopup().Alert().Title(Equals("Bisect complete")).Content(MatchesRegexp("(?s)commit 08.*Do you want to reset")).Confirm() - assert.CurrentView().TopLines( - MatchesRegexp(`<-- bad.*commit 08`), - MatchesRegexp(`<-- current.*commit 07`), - MatchesRegexp(`\?.*commit 06`), - MatchesRegexp(`<-- good.*commit 05`), - ) - - input.NextItem() - - input.Press(keys.Commits.ViewBisectOptions) - input.Menu(Equals("Bisect"), MatchesRegexp(`mark .* as good`)) - - input.Alert(Equals("Bisect complete"), MatchesRegexp(`(?s)commit 08.*Do you want to reset`)) - - assert.View("information").Content(DoesNotContain("bisecting")) - - // back in master branch which just had the one commit - assert.CurrentView().Name("commits").Lines( - Contains("only commit on master"), - ) + t.Views().Information().Content(DoesNotContain("bisecting")) + }). + // back in master branch which just had the one commit + Lines( + Contains("only commit on master"), + ) }, }) diff --git a/pkg/integration/tests/branch/checkout_by_name.go b/pkg/integration/tests/branch/checkout_by_name.go index d3742e8f9..070a3a433 100644 --- a/pkg/integration/tests/branch/checkout_by_name.go +++ b/pkg/integration/tests/branch/checkout_by_name.go @@ -17,27 +17,24 @@ var CheckoutByName = NewIntegrationTest(NewIntegrationTestArgs{ Checkout("master"). EmptyCommit("blah") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() - - assert.CurrentView().Lines( - Contains("master"), - Contains("@"), - ) - input.NextItem() - - input.Press(keys.Branches.CheckoutBranchByName) - - input.Prompt(Equals("Branch name:"), "new-branch") - - input.Alert(Equals("Branch not found"), Equals("Branch not found. Create a new branch named new-branch?")) - - assert.CurrentView().Name("localBranches"). + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). Lines( - MatchesRegexp(`\*.*new-branch`), - Contains("master"), + Contains("master").IsSelected(), Contains("@"), ). - SelectedLine(Contains("new-branch")) + SelectNextItem(). + Press(keys.Branches.CheckoutBranchByName). + Tap(func() { + t.ExpectPopup().Prompt().Title(Equals("Branch name:")).Type("new-branch").Confirm() + + t.ExpectPopup().Alert().Title(Equals("Branch not found")).Content(Equals("Branch not found. Create a new branch named new-branch?")).Confirm() + }). + Lines( + MatchesRegexp(`\*.*new-branch`).IsSelected(), + Contains("master"), + Contains("@"), + ) }, }) diff --git a/pkg/integration/tests/branch/delete.go b/pkg/integration/tests/branch/delete.go index 01bd4edda..7d93513dc 100644 --- a/pkg/integration/tests/branch/delete.go +++ b/pkg/integration/tests/branch/delete.go @@ -16,24 +16,26 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ NewBranch("branch-one"). NewBranch("branch-two") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() - - assert.CurrentView().Lines( - MatchesRegexp(`\*.*branch-two`), - MatchesRegexp(`branch-one`), - MatchesRegexp(`master`), - ) - - input.Press(keys.Universal.Remove) - input.Alert(Equals("Error"), Contains("You cannot delete the checked out branch!")) - - input.NextItem() - - input.Press(keys.Universal.Remove) - input.AcceptConfirmation(Equals("Delete Branch"), Contains("Are you sure you want to delete the branch 'branch-one'?")) - - assert.CurrentView().Name("localBranches"). + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + MatchesRegexp(`\*.*branch-two`).IsSelected(), + MatchesRegexp(`branch-one`), + MatchesRegexp(`master`), + ). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Alert().Title(Equals("Error")).Content(Contains("You cannot delete the checked out branch!")).Confirm() + }). + SelectNextItem(). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Delete Branch")). + Content(Contains("Are you sure you want to delete the branch 'branch-one'?")). + Confirm() + }). Lines( MatchesRegexp(`\*.*branch-two`), MatchesRegexp(`master`).IsSelected(), diff --git a/pkg/integration/tests/branch/rebase.go b/pkg/integration/tests/branch/rebase.go index 860331215..e59aa8cb2 100644 --- a/pkg/integration/tests/branch/rebase.go +++ b/pkg/integration/tests/branch/rebase.go @@ -14,42 +14,51 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{ SetupRepo: func(shell *Shell) { shared.MergeConflictsSetup(shell) }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() - - assert.View("localBranches").Lines( - Contains("first-change-branch"), - Contains("second-change-branch"), - Contains("original-branch"), - ) - - assert.View("commits").TopLines( + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits().TopLines( Contains("first change"), Contains("original"), ) - input.NextItem() - input.Press(keys.Branches.RebaseBranch) + t.Views().Branches(). + Focus(). + Lines( + Contains("first-change-branch"), + Contains("second-change-branch"), + Contains("original-branch"), + ). + SelectNextItem(). + Press(keys.Branches.RebaseBranch) - input.AcceptConfirmation(Equals("Rebasing"), Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")) - input.AcceptConfirmation(Equals("Auto-merge failed"), Contains("Conflicts!")) + t.ExpectPopup().Confirmation(). + Title(Equals("Rebasing")). + Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")). + Confirm() - assert.CurrentView().Name("files").SelectedLine(Contains("file")) + t.ExpectPopup().Confirmation(). + Title(Equals("Auto-merge failed")). + Content(Contains("Conflicts!")). + Confirm() - // not using Confirm() convenience method because I suspect we might change this - // keybinding to something more bespoke - input.Press(keys.Universal.Confirm) + t.Views().Files(). + IsFocused(). + SelectedLine(Contains("file")). + PressEnter() - assert.CurrentView().Name("mergeConflicts") - input.PrimaryAction() + t.Views().MergeConflicts(). + IsFocused(). + PressPrimaryAction() - assert.View("information").Content(Contains("rebasing")) + t.Views().Information().Content(Contains("rebasing")) - input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?")) + t.ExpectPopup().Confirmation(). + Title(Equals("continue")). + Content(Contains("all merge conflicts resolved. Continue?")). + Confirm() - assert.View("information").Content(DoesNotContain("rebasing")) + t.Views().Information().Content(DoesNotContain("rebasing")) - assert.View("commits").TopLines( + t.Views().Commits().TopLines( Contains("second-change-branch unrelated change"), Contains("second change"), Contains("original"), diff --git a/pkg/integration/tests/branch/rebase_and_drop.go b/pkg/integration/tests/branch/rebase_and_drop.go index 2ef65c3aa..5f5341d66 100644 --- a/pkg/integration/tests/branch/rebase_and_drop.go +++ b/pkg/integration/tests/branch/rebase_and_drop.go @@ -17,49 +17,51 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{ shell.EmptyCommit("to remove") shell.EmptyCommit("to keep") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + TopLines( + Contains("to keep"), + Contains("to remove"), + Contains("first change"), + Contains("original"), + ) - assert.CurrentView().Lines( - Contains("first-change-branch"), - Contains("second-change-branch"), - Contains("original-branch"), - ) + t.Views().Branches(). + Focus(). + Lines( + Contains("first-change-branch").IsSelected(), + Contains("second-change-branch"), + Contains("original-branch"), + ). + SelectNextItem(). + Press(keys.Branches.RebaseBranch) - assert.View("commits").TopLines( - Contains("to keep").IsSelected(), - Contains("to remove"), - Contains("first change"), - Contains("original"), - ) + t.ExpectPopup().Confirmation(). + Title(Equals("Rebasing")). + Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")). + Confirm() - input.NextItem() - input.Press(keys.Branches.RebaseBranch) + t.Views().Information().Content(Contains("rebasing")) - input.AcceptConfirmation(Equals("Rebasing"), Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")) + t.ExpectPopup().Confirmation(). + Title(Equals("Auto-merge failed")). + Content(Contains("Conflicts!")). + Confirm() - assert.View("information").Content(Contains("rebasing")) - - input.AcceptConfirmation(Equals("Auto-merge failed"), Contains("Conflicts!")) - - assert.CurrentView(). - Name("files"). + t.Views().Files().IsFocused(). SelectedLine(MatchesRegexp("UU.*file")) - input.SwitchToCommitsView() - assert.CurrentView(). + t.Views().Commits(). + Focus(). TopLines( MatchesRegexp(`pick.*to keep`).IsSelected(), MatchesRegexp(`pick.*to remove`), MatchesRegexp("YOU ARE HERE.*second-change-branch unrelated change"), MatchesRegexp("second change"), MatchesRegexp("original"), - ) - - input.NextItem() - input.Press(keys.Universal.Remove) - - assert.CurrentView(). + ). + SelectNextItem(). + Press(keys.Universal.Remove). TopLines( MatchesRegexp(`pick.*to keep`), MatchesRegexp(`drop.*to remove`).IsSelected(), @@ -68,20 +70,22 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{ MatchesRegexp("original"), ) - input.SwitchToFilesView() + t.Views().Files(). + Focus(). + PressEnter() - // not using Confirm() convenience method because I suspect we might change this - // keybinding to something more bespoke - input.Press(keys.Universal.Confirm) + t.Views().MergeConflicts(). + IsFocused(). + PressPrimaryAction() - assert.CurrentView().Name("mergeConflicts") - input.PrimaryAction() + t.ExpectPopup().Confirmation(). + Title(Equals("continue")). + Content(Contains("all merge conflicts resolved. Continue?")). + Confirm() - input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?")) + t.Views().Information().Content(DoesNotContain("rebasing")) - assert.View("information").Content(DoesNotContain("rebasing")) - - assert.View("commits").TopLines( + t.Views().Commits().TopLines( Contains("to keep"), Contains("second-change-branch unrelated change").IsSelected(), Contains("second change"), diff --git a/pkg/integration/tests/branch/reset.go b/pkg/integration/tests/branch/reset.go index 8be35e4ec..0a46ad2a1 100644 --- a/pkg/integration/tests/branch/reset.go +++ b/pkg/integration/tests/branch/reset.go @@ -20,32 +20,31 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{ shell.Checkout("current-branch") shell.EmptyCommit("current-branch commit") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.View("commits").Lines( + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits().Lines( Contains("current-branch commit"), Contains("root commit"), ) - input.SwitchToBranchesView() + t.Views().Branches(). + Focus(). + Lines( + Contains("current-branch").IsSelected(), + Contains("other-branch"), + ). + SelectNextItem(). + Press(keys.Commits.ViewResetOptions) - assert.CurrentView().Lines( - Contains("current-branch"), - Contains("other-branch"), - ) - input.NextItem() - - input.Press(keys.Commits.ViewResetOptions) - - input.Menu(Contains("reset to other-branch"), Contains("hard reset")) - - // ensure that we've returned from the menu before continuing - assert.CurrentView().Name("localBranches") + t.ExpectPopup().Menu(). + Title(Contains("reset to other-branch")). + Select(Contains("hard reset")). + Confirm() // assert that we now have the expected commits in the commit panel - input.SwitchToCommitsView() - assert.CurrentView().Lines( - Contains("other-branch commit"), - Contains("root commit"), - ) + t.Views().Commits(). + Lines( + Contains("other-branch commit"), + Contains("root commit"), + ) }, }) diff --git a/pkg/integration/tests/branch/suggestions.go b/pkg/integration/tests/branch/suggestions.go index 7631ad735..5b5b23403 100644 --- a/pkg/integration/tests/branch/suggestions.go +++ b/pkg/integration/tests/branch/suggestions.go @@ -20,15 +20,19 @@ var Suggestions = NewIntegrationTest(NewIntegrationTestArgs{ NewBranch("other-new-branch-2"). NewBranch("other-new-branch-3") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() - - input.Press(keys.Branches.CheckoutBranchByName) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Press(keys.Branches.CheckoutBranchByName) // we expect the first suggestion to be the branch we want because it most // closely matches what we typed in - input.Typeahead(Equals("Branch name:"), "branch-to", Contains("branch-to-checkout")) + t.ExpectPopup().Prompt(). + Title(Equals("Branch name:")). + Type("branch-to"). + SuggestionTopLines(Contains("branch-to-checkout")). + ConfirmFirstSuggestion() - assert.CurrentBranchName("branch-to-checkout") + t.Git().CurrentBranchName("branch-to-checkout") }, }) diff --git a/pkg/integration/tests/cherry_pick/cherry_pick.go b/pkg/integration/tests/cherry_pick/cherry_pick.go index e79e35b19..7aaaf36f1 100644 --- a/pkg/integration/tests/cherry_pick/cherry_pick.go +++ b/pkg/integration/tests/cherry_pick/cherry_pick.go @@ -23,53 +23,62 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{ EmptyCommit("four"). Checkout("first-branch") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("first-branch"), + Contains("second-branch"), + Contains("master"), + ). + SelectNextItem(). + PressEnter() - assert.CurrentView().Lines( - Contains("first-branch"), - Contains("second-branch"), - Contains("master"), - ) + t.Views().SubCommits(). + IsFocused(). + Lines( + Contains("four").IsSelected(), + Contains("three"), + Contains("base"), + ). + // copy commits 'four' and 'three' + Press(keys.Commits.CherryPickCopy). + Tap(func() { + t.Views().Information().Content(Contains("1 commit copied")) + }). + SelectNextItem(). + Press(keys.Commits.CherryPickCopy) - input.NextItem() + t.Views().Information().Content(Contains("2 commits copied")) - input.Enter() - - assert.CurrentView().Name("subCommits").Lines( - Contains("four"), - Contains("three"), - Contains("base"), - ) - - // copy commits 'four' and 'three' - input.Press(keys.Commits.CherryPickCopy) - assert.View("information").Content(Contains("1 commit copied")) - input.NextItem() - input.Press(keys.Commits.CherryPickCopy) - assert.View("information").Content(Contains("2 commits copied")) - - input.SwitchToCommitsView() - - assert.CurrentView().Lines( - Contains("two"), - Contains("one"), - Contains("base"), - ) - - input.Press(keys.Commits.PasteCommits) - input.Alert(Equals("Cherry-Pick"), Contains("Are you sure you want to cherry-pick the copied commits onto this branch?")) - - assert.CurrentView().Name("commits").Lines( - Contains("four"), - Contains("three"), - Contains("two"), - Contains("one"), - Contains("base"), - ) - - assert.View("information").Content(Contains("2 commits copied")) - input.Press(keys.Universal.Return) - assert.View("information").Content(DoesNotContain("commits copied")) + t.Views().Commits(). + Focus(). + Lines( + Contains("two").IsSelected(), + Contains("one"), + Contains("base"), + ). + Press(keys.Commits.PasteCommits). + Tap(func() { + t.ExpectPopup().Alert(). + Title(Equals("Cherry-Pick")). + Content(Contains("Are you sure you want to cherry-pick the copied commits onto this branch?")). + Confirm() + }). + Lines( + Contains("four"), + Contains("three"), + Contains("two"), + Contains("one"), + Contains("base"), + ). + Tap(func() { + // we need to manually exit out of cherry pick mode + t.Views().Information().Content(Contains("2 commits copied")) + }). + PressEscape(). + Tap(func() { + t.Views().Information().Content(DoesNotContain("commits copied")) + }) }, }) diff --git a/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go b/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go index 4ed310e1d..df9988c2a 100644 --- a/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go +++ b/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go @@ -14,75 +14,85 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{ SetupRepo: func(shell *Shell) { shared.MergeConflictsSetup(shell) }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() - assert.CurrentView().Lines( - Contains("first-change-branch"), - Contains("second-change-branch"), - Contains("original-branch"), - ) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("first-change-branch"), + Contains("second-change-branch"), + Contains("original-branch"), + ). + SelectNextItem(). + PressEnter() - input.NextItem() + t.Views().SubCommits(). + IsFocused(). + TopLines( + Contains("second-change-branch unrelated change"), + Contains("second change"), + ). + Press(keys.Commits.CherryPickCopy). + Tap(func() { + t.Views().Information().Content(Contains("1 commit copied")) + }). + SelectNextItem(). + Press(keys.Commits.CherryPickCopy) - input.Enter() + t.Views().Information().Content(Contains("2 commits copied")) - assert.CurrentView().Name("subCommits").TopLines( - Contains("second-change-branch unrelated change"), - Contains("second change"), - ) + t.Views().Commits(). + Focus(). + TopLines( + Contains("first change"), + ). + Press(keys.Commits.PasteCommits) - input.Press(keys.Commits.CherryPickCopy) - assert.View("information").Content(Contains("1 commit copied")) + t.ExpectPopup().Alert().Title(Equals("Cherry-Pick")).Content(Contains("Are you sure you want to cherry-pick the copied commits onto this branch?")).Confirm() - input.NextItem() - input.Press(keys.Commits.CherryPickCopy) - assert.View("information").Content(Contains("2 commits copied")) + t.ExpectPopup().Confirmation(). + Title(Equals("Auto-merge failed")). + Content(Contains("Conflicts!")). + Confirm() - input.SwitchToCommitsView() + t.Views().Files(). + IsFocused(). + SelectedLine(Contains("file")). + PressEnter() - assert.CurrentView().TopLines( - Contains("first change"), - ) + t.Views().MergeConflicts(). + IsFocused(). + // picking 'Second change' + SelectNextItem(). + PressPrimaryAction() - input.Press(keys.Commits.PasteCommits) - input.Alert(Equals("Cherry-Pick"), Contains("Are you sure you want to cherry-pick the copied commits onto this branch?")) + t.ExpectPopup().Confirmation(). + Title(Equals("continue")). + Content(Contains("all merge conflicts resolved. Continue?")). + Confirm() - input.AcceptConfirmation(Equals("Auto-merge failed"), Contains("Conflicts!")) + t.Views().Files().IsEmpty() - assert.CurrentView().Name("files") - assert.CurrentView().SelectedLine(Contains("file")) + t.Views().Commits(). + Focus(). + TopLines( + Contains("second-change-branch unrelated change"), + Contains("second change"), + Contains("first change"), + ). + SelectNextItem(). + Tap(func() { + // because we picked 'Second change' when resolving the conflict, + // we now see this commit as having replaced First Change with Second Change, + // as opposed to replacing 'Original' with 'Second change' + t.Views().Main(). + Content(Contains("-First Change")). + Content(Contains("+Second Change")) - // not using Confirm() convenience method because I suspect we might change this - // keybinding to something more bespoke - input.Press(keys.Universal.Confirm) - - assert.CurrentView().Name("mergeConflicts") - // picking 'Second change' - input.NextItem() - input.PrimaryAction() - - input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?")) - - assert.CurrentView().Name("files") - assert.WorkingTreeFileCount(0) - - input.SwitchToCommitsView() - - assert.CurrentView().TopLines( - Contains("second-change-branch unrelated change"), - Contains("second change"), - Contains("first change"), - ) - input.NextItem() - // because we picked 'Second change' when resolving the conflict, - // we now see this commit as having replaced First Change with Second Change, - // as opposed to replacing 'Original' with 'Second change' - assert.MainView(). - Content(Contains("-First Change")). - Content(Contains("+Second Change")) - - assert.View("information").Content(Contains("2 commits copied")) - input.Press(keys.Universal.Return) - assert.View("information").Content(DoesNotContain("commits copied")) + t.Views().Information().Content(Contains("2 commits copied")) + }). + PressEscape(). + Tap(func() { + t.Views().Information().Content(DoesNotContain("commits copied")) + }) }, }) diff --git a/pkg/integration/tests/commit/commit.go b/pkg/integration/tests/commit/commit.go index a20b0df34..6ed75a797 100644 --- a/pkg/integration/tests/commit/commit.go +++ b/pkg/integration/tests/commit/commit.go @@ -14,20 +14,24 @@ var Commit = NewIntegrationTest(NewIntegrationTestArgs{ shell.CreateFile("myfile", "myfile content") shell.CreateFile("myfile2", "myfile2 content") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() - input.PrimaryAction() - input.NextItem() - input.PrimaryAction() - input.Press(keys.Files.CommitChanges) + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). // stage file + SelectNextItem(). + PressPrimaryAction(). // stage other file + Press(keys.Files.CommitChanges) - assert.InCommitMessagePanel() commitMessage := "my commit message" - input.Type(commitMessage) - input.Confirm() - assert.CommitCount(1) - assert.HeadCommitMessage(Equals(commitMessage)) + t.ExpectPopup().CommitMessagePanel().Type(commitMessage).Confirm() + + t.Views().Commits(). + Lines( + Contains(commitMessage), + ) }, }) diff --git a/pkg/integration/tests/commit/commit_multiline.go b/pkg/integration/tests/commit/commit_multiline.go index d19e09a72..4967ffb77 100644 --- a/pkg/integration/tests/commit/commit_multiline.go +++ b/pkg/integration/tests/commit/commit_multiline.go @@ -13,23 +13,23 @@ var CommitMultiline = NewIntegrationTest(NewIntegrationTestArgs{ SetupRepo: func(shell *Shell) { shell.CreateFile("myfile", "myfile content") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() - input.PrimaryAction() - input.Press(keys.Files.CommitChanges) + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). + Press(keys.Files.CommitChanges) - assert.InCommitMessagePanel() - input.Type("first line") - input.Press(keys.Universal.AppendNewline) - input.Press(keys.Universal.AppendNewline) - input.Type("third line") - input.Confirm() + t.ExpectPopup().CommitMessagePanel().Type("first line").AddNewline().AddNewline().Type("third line").Confirm() - assert.CommitCount(1) - assert.HeadCommitMessage(Equals("first line")) + t.Views().Commits(). + Lines( + Contains("first line"), + ) - input.SwitchToCommitsView() - assert.MainView().Content(MatchesRegexp("first line\n\\s*\n\\s*third line")) + t.Views().Commits().Focus() + t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*third line")) }, }) diff --git a/pkg/integration/tests/commit/new_branch.go b/pkg/integration/tests/commit/new_branch.go index 1702b0973..d3cd58f23 100644 --- a/pkg/integration/tests/commit/new_branch.go +++ b/pkg/integration/tests/commit/new_branch.go @@ -16,27 +16,25 @@ var NewBranch = NewIntegrationTest(NewIntegrationTestArgs{ EmptyCommit("commit 2"). EmptyCommit("commit 3") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(3) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("commit 3").IsSelected(), + Contains("commit 2"), + Contains("commit 1"), + ). + SelectNextItem(). + Press(keys.Universal.New). + Tap(func() { + branchName := "my-branch-name" + t.ExpectPopup().Prompt().Title(Contains("New Branch Name")).Type(branchName).Confirm() - input.SwitchToCommitsView() - assert.CurrentView().Lines( - Contains("commit 3"), - Contains("commit 2"), - Contains("commit 1"), - ) - input.NextItem() - - input.Press(keys.Universal.New) - - branchName := "my-branch-name" - input.Prompt(Contains("New Branch Name"), branchName) - - assert.CurrentBranchName(branchName) - - assert.View("commits").Lines( - Contains("commit 2"), - Contains("commit 1"), - ) + t.Git().CurrentBranchName(branchName) + }). + Lines( + Contains("commit 2"), + Contains("commit 1"), + ) }, }) diff --git a/pkg/integration/tests/commit/revert.go b/pkg/integration/tests/commit/revert.go index a9bd6373a..3b55aa65d 100644 --- a/pkg/integration/tests/commit/revert.go +++ b/pkg/integration/tests/commit/revert.go @@ -15,25 +15,25 @@ var Revert = NewIntegrationTest(NewIntegrationTestArgs{ shell.GitAddAll() shell.Commit("first commit") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(1) - - input.SwitchToCommitsView() - - assert.CurrentView().Lines( - Contains("first commit"), - ) - - input.Press(keys.Commits.RevertCommit) - input.AcceptConfirmation(Equals("Revert commit"), MatchesRegexp(`Are you sure you want to revert \w+?`)) - - assert.CurrentView().Name("commits"). + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("first commit"), + ). + Press(keys.Commits.RevertCommit). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Revert commit")). + Content(MatchesRegexp(`Are you sure you want to revert \w+?`)). + Confirm() + }). Lines( Contains("Revert \"first commit\"").IsSelected(), Contains("first commit"), ) - assert.MainView().Content(Contains("-myfile content")) - assert.FileSystemPathNotPresent("myfile") + t.Views().Main().Content(Contains("-myfile content")) + t.FileSystem().PathNotPresent("myfile") }, }) diff --git a/pkg/integration/tests/commit/staged.go b/pkg/integration/tests/commit/staged.go index 4a0c3eafc..09bcf2815 100644 --- a/pkg/integration/tests/commit/staged.go +++ b/pkg/integration/tests/commit/staged.go @@ -15,38 +15,45 @@ var Staged = NewIntegrationTest(NewIntegrationTestArgs{ CreateFile("myfile", "myfile content\nwith a second line"). CreateFile("myfile2", "myfile2 content") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() - assert.CurrentView().Name("files") - assert.CurrentView().SelectedLine(Contains("myfile")) - // stage the file - input.PrimaryAction() - 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(DoesNotContain("+myfile content")) - assert.View("staging").Content(DoesNotContain("+with a second line")) + t.Views().Files(). + IsFocused(). + SelectedLine(Contains("myfile")). + PressPrimaryAction(). // stage the file + PressEnter() - // unstage the selected line - input.PrimaryAction() + t.Views().StagingSecondary(). + IsFocused(). + Tap(func() { + // we start with both lines having been staged + t.Views().StagingSecondary().Content(Contains("+myfile content")) + t.Views().StagingSecondary().Content(Contains("+with a second line")) + t.Views().Staging().Content(DoesNotContain("+myfile content")) + t.Views().Staging().Content(DoesNotContain("+with a second line")) + }). + // unstage the selected line + PressPrimaryAction(). + Tap(func() { + // the line should have been moved to the main view + t.Views().StagingSecondary().Content(DoesNotContain("+myfile content")) + t.Views().StagingSecondary().Content(Contains("+with a second line")) + t.Views().Staging().Content(Contains("+myfile content")) + t.Views().Staging().Content(DoesNotContain("+with a second line")) + }). + Press(keys.Files.CommitChanges) - // the line should have been moved to the main view - 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(DoesNotContain("+with a second line")) - - input.Press(keys.Files.CommitChanges) commitMessage := "my commit message" - input.Type(commitMessage) - input.Confirm() + t.ExpectPopup().CommitMessagePanel().Type(commitMessage).Confirm() - assert.CommitCount(1) - assert.HeadCommitMessage(Equals(commitMessage)) - assert.CurrentWindowName("stagingSecondary") + t.Views().Commits(). + Lines( + Contains(commitMessage), + ) + + t.Views().StagingSecondary().IsFocused() // TODO: assert that the staging panel has been refreshed (it currently does not get correctly refreshed) }, diff --git a/pkg/integration/tests/commit/staged_without_hooks.go b/pkg/integration/tests/commit/staged_without_hooks.go index e2f0752f0..620f712f9 100644 --- a/pkg/integration/tests/commit/staged_without_hooks.go +++ b/pkg/integration/tests/commit/staged_without_hooks.go @@ -15,40 +15,45 @@ var StagedWithoutHooks = NewIntegrationTest(NewIntegrationTestArgs{ CreateFile("myfile", "myfile content\nwith a second line"). CreateFile("myfile2", "myfile2 content") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() // stage the file - assert.CurrentView().Name("files") - assert.CurrentView().SelectedLine(Contains("myfile")) - input.PrimaryAction() - input.Enter() - assert.CurrentView().Name("stagingSecondary") + t.Views().Files(). + IsFocused(). + SelectedLine(Contains("myfile")). + PressPrimaryAction(). + PressEnter() + // we start with both lines having been staged - assert.View("stagingSecondary").Content( + t.Views().StagingSecondary().Content( Contains("+myfile content").Contains("+with a second line"), ) - assert.View("staging").Content( + t.Views().Staging().Content( DoesNotContain("+myfile content").DoesNotContain("+with a second line"), ) // unstage the selected line - input.PrimaryAction() + t.Views().StagingSecondary(). + IsFocused(). + PressPrimaryAction(). + Tap(func() { + // the line should have been moved to the main view + t.Views().Staging().Content(Contains("+myfile content").DoesNotContain("+with a second line")) + }). + Content(DoesNotContain("+myfile content").Contains("+with a second line")). + Press(keys.Files.CommitChangesWithoutHook) - // the line should have been moved to the main view - 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() - assert.CurrentView().Content(Contains("WIP")) commitMessage := ": my commit message" - input.Type(commitMessage) - input.Confirm() + t.ExpectPopup().CommitMessagePanel().InitialText(Contains("WIP")).Type(commitMessage).Confirm() - assert.CommitCount(1) - assert.HeadCommitMessage(Equals("WIP" + commitMessage)) - assert.CurrentView().Name("stagingSecondary") + t.Views().Commits(). + Lines( + Contains("WIP" + commitMessage), + ) + + t.Views().StagingSecondary().IsFocused() // TODO: assert that the staging panel has been refreshed (it currently does not get correctly refreshed) }, diff --git a/pkg/integration/tests/commit/unstaged.go b/pkg/integration/tests/commit/unstaged.go index 6e7e2a307..c0e26b281 100644 --- a/pkg/integration/tests/commit/unstaged.go +++ b/pkg/integration/tests/commit/unstaged.go @@ -5,7 +5,7 @@ import ( . "github.com/jesseduffield/lazygit/pkg/integration/components" ) -// TODO: find out why we can't use assert.SelectedLine() on the staging/stagingSecondary views. +// TODO: find out why we can't use .SelectedLine() on the staging/stagingSecondary views. var Unstaged = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Staging a couple files, going in the unstaged files menu, staging a line and committing", @@ -17,27 +17,37 @@ var Unstaged = NewIntegrationTest(NewIntegrationTestArgs{ CreateFile("myfile", "myfile content\nwith a second line"). CreateFile("myfile2", "myfile2 content") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() - assert.CurrentView().Name("files").SelectedLine(Contains("myfile")) - input.Enter() - assert.CurrentView().Name("staging") - assert.View("stagingSecondary").Content(DoesNotContain("+myfile content")) - // stage the first line - input.PrimaryAction() - assert.View("staging").Content(DoesNotContain("+myfile content")) - assert.View("stagingSecondary").Content(Contains("+myfile content")) + t.Views().Files(). + IsFocused(). + SelectedLine(Contains("myfile")). + PressEnter() + + t.Views().Staging(). + IsFocused(). + Tap(func() { + t.Views().StagingSecondary().Content(DoesNotContain("+myfile content")) + }). + // stage the first line + PressPrimaryAction(). + Tap(func() { + t.Views().Staging().Content(DoesNotContain("+myfile content")) + t.Views().StagingSecondary().Content(Contains("+myfile content")) + }). + Press(keys.Files.CommitChanges) - input.Press(keys.Files.CommitChanges) - assert.InCommitMessagePanel() commitMessage := "my commit message" - input.Type(commitMessage) - input.Confirm() + t.ExpectPopup().CommitMessagePanel().Type(commitMessage).Confirm() - assert.CommitCount(1) - assert.HeadCommitMessage(Equals(commitMessage)) - assert.CurrentWindowName("staging") + t.Views().Commits(). + Lines( + Contains(commitMessage), + ) + + t.Views().Staging().IsFocused() // TODO: assert that the staging panel has been refreshed (it currently does not get correctly refreshed) }, diff --git a/pkg/integration/tests/config/remote_named_star.go b/pkg/integration/tests/config/remote_named_star.go index fd28dea7b..735389667 100644 --- a/pkg/integration/tests/config/remote_named_star.go +++ b/pkg/integration/tests/config/remote_named_star.go @@ -15,13 +15,12 @@ var RemoteNamedStar = NewIntegrationTest(NewIntegrationTestArgs{ CreateNCommits(2) }, SetupConfig: func(cfg *config.AppConfig) {}, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { + Run: func(t *TestDriver, keys config.KeybindingConfig) { // here we're just asserting that we haven't panicked upon starting lazygit - assert.AtLeastOneCommit() + t.Views().Commits(). + Lines( + Anything(), + Anything(), + ) }, }) diff --git a/pkg/integration/tests/custom_commands/basic.go b/pkg/integration/tests/custom_commands/basic.go index fc3cad569..ce500b9a6 100644 --- a/pkg/integration/tests/custom_commands/basic.go +++ b/pkg/integration/tests/custom_commands/basic.go @@ -21,18 +21,13 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ }, } }, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { - assert.WorkingTreeFileCount(0) - - input.Press("a") - - assert.View("files").Lines( - Contains("myfile"), - ) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsEmpty(). + IsFocused(). + Press("a"). + Lines( + Contains("myfile"), + ) }, }) diff --git a/pkg/integration/tests/custom_commands/form_prompts.go b/pkg/integration/tests/custom_commands/form_prompts.go index 8651efd39..dcb41c83e 100644 --- a/pkg/integration/tests/custom_commands/form_prompts.go +++ b/pkg/integration/tests/custom_commands/form_prompts.go @@ -55,24 +55,26 @@ var FormPrompts = NewIntegrationTest(NewIntegrationTestArgs{ }, } }, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { - assert.WorkingTreeFileCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsEmpty(). + IsFocused(). + Press("a") - input.Press("a") + t.ExpectPopup().Prompt().Title(Equals("Enter a file name")).Type("my file").Confirm() - input.Prompt(Equals("Enter a file name"), "my file") + t.ExpectPopup().Menu().Title(Equals("Choose file content")).Select(Contains("bar")).Confirm() - input.Menu(Equals("Choose file content"), Contains("bar")) + t.ExpectPopup().Confirmation(). + Title(Equals("Are you sure?")). + Content(Equals("Are you REALLY sure you want to make this file? Up to you buddy.")). + Confirm() - input.AcceptConfirmation(Equals("Are you sure?"), Equals("Are you REALLY sure you want to make this file? Up to you buddy.")) + t.Views().Files(). + Lines( + Contains("my file").IsSelected(), + ) - assert.WorkingTreeFileCount(1) - assert.CurrentView().SelectedLine(Contains("my file")) - assert.MainView().Content(Contains(`"BAR"`)) + t.Views().Main().Content(Contains(`"BAR"`)) }, }) diff --git a/pkg/integration/tests/custom_commands/menu_from_command.go b/pkg/integration/tests/custom_commands/menu_from_command.go index c4fe063e2..db856c807 100644 --- a/pkg/integration/tests/custom_commands/menu_from_command.go +++ b/pkg/integration/tests/custom_commands/menu_from_command.go @@ -42,25 +42,24 @@ var MenuFromCommand = NewIntegrationTest(NewIntegrationTestArgs{ }, } }, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { - assert.WorkingTreeFileCount(0) - input.SwitchToBranchesView() + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsEmpty() - input.Press("a") + t.Views().Branches(). + Focus(). + Press("a") - input.Menu(Equals("Choose commit message"), Contains("bar")) + t.ExpectPopup().Menu().Title(Equals("Choose commit message")).Select(Contains("bar")).Confirm() - input.Prompt(Equals("Description"), " my branch") + t.ExpectPopup().Prompt().Title(Equals("Description")).Type(" my branch").Confirm() - input.SwitchToFilesView() + t.Views().Files(). + Focus(). + Lines( + Contains("output.txt").IsSelected(), + ) - assert.WorkingTreeFileCount(1) - assert.CurrentView().SelectedLine(Contains("output.txt")) - assert.MainView().Content(Contains("bar Branch: #feature/foo my branch feature/foo")) + t.Views().Main().Content(Contains("bar Branch: #feature/foo my branch feature/foo")) }, }) diff --git a/pkg/integration/tests/custom_commands/menu_from_commands_output.go b/pkg/integration/tests/custom_commands/menu_from_commands_output.go index 68c4db0f6..123e27695 100644 --- a/pkg/integration/tests/custom_commands/menu_from_commands_output.go +++ b/pkg/integration/tests/custom_commands/menu_from_commands_output.go @@ -41,27 +41,20 @@ var MenuFromCommandsOutput = NewIntegrationTest(NewIntegrationTestArgs{ }, } }, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { - assert.CurrentBranchName("feature/bar") + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Git().CurrentBranchName("feature/bar") - assert.WorkingTreeFileCount(0) - input.SwitchToBranchesView() + t.Views().Branches(). + Focus(). + Press("a") - input.Press("a") - - assert.InPrompt() - assert.CurrentView(). + t.ExpectPopup().Prompt(). Title(Equals("Which git command do you want to run?")). - SelectedLine(Equals("branch")) - input.Confirm() + InitialText(Equals("branch")). + Confirm() - input.Menu(Equals("Branch:"), Equals("master")) + t.ExpectPopup().Menu().Title(Equals("Branch:")).Select(Equals("master")).Confirm() - assert.CurrentBranchName("master") + t.Git().CurrentBranchName("master") }, }) diff --git a/pkg/integration/tests/custom_commands/multiple_prompts.go b/pkg/integration/tests/custom_commands/multiple_prompts.go index f3aa04728..b1592d03b 100644 --- a/pkg/integration/tests/custom_commands/multiple_prompts.go +++ b/pkg/integration/tests/custom_commands/multiple_prompts.go @@ -53,24 +53,27 @@ var MultiplePrompts = NewIntegrationTest(NewIntegrationTestArgs{ }, } }, - Run: func( - shell *Shell, - input *Input, - assert *Assert, - keys config.KeybindingConfig, - ) { - assert.WorkingTreeFileCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsEmpty(). + IsFocused(). + Press("a") - input.Press("a") + t.ExpectPopup().Prompt().Title(Equals("Enter a file name")).Type("myfile").Confirm() - input.Prompt(Equals("Enter a file name"), "myfile") + t.ExpectPopup().Menu().Title(Equals("Choose file content")).Select(Contains("bar")).Confirm() - input.Menu(Equals("Choose file content"), Contains("bar")) + t.ExpectPopup().Confirmation(). + Title(Equals("Are you sure?")). + Content(Equals("Are you REALLY sure you want to make this file? Up to you buddy.")). + Confirm() - input.AcceptConfirmation(Equals("Are you sure?"), Equals("Are you REALLY sure you want to make this file? Up to you buddy.")) + t.Views().Files(). + Focus(). + Lines( + Contains("myfile").IsSelected(), + ) - assert.WorkingTreeFileCount(1) - assert.CurrentView().SelectedLine(Contains("myfile")) - assert.MainView().Content(Contains("BAR")) + t.Views().Main().Content(Contains("BAR")) }, }) diff --git a/pkg/integration/tests/diff/diff.go b/pkg/integration/tests/diff/diff.go index 4bba36de8..8730fd881 100644 --- a/pkg/integration/tests/diff/diff.go +++ b/pkg/integration/tests/diff/diff.go @@ -21,38 +21,53 @@ var Diff = NewIntegrationTest(NewIntegrationTestArgs{ shell.Checkout("branch-a") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + TopLines( + Contains("branch-a"), + Contains("branch-b"), + ). + Press(keys.Universal.DiffingMenu) - assert.CurrentView().TopLines( - Contains("branch-a"), - Contains("branch-b"), - ) - input.Press(keys.Universal.DiffingMenu) - input.Menu(Equals("Diffing"), Contains(`diff branch-a`)) + t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains(`diff branch-a`)).Confirm() - assert.CurrentView().Name("localBranches") + t.Views().Branches(). + IsFocused(). + Tap(func() { + t.Views().Information().Content(Contains("showing output for: git diff branch-a branch-a")) + }). + SelectNextItem(). + Tap(func() { + t.Views().Information().Content(Contains("showing output for: git diff branch-a branch-b")) + t.Views().Main().Content(Contains("+second line")) + }). + PressEnter() - assert.View("information").Content(Contains("showing output for: git diff branch-a branch-a")) - input.NextItem() - assert.View("information").Content(Contains("showing output for: git diff branch-a branch-b")) - assert.MainView().Content(Contains("+second line")) + t.Views().SubCommits(). + IsFocused(). + SelectedLine(Contains("update")). + Tap(func() { + t.Views().Main().Content(Contains("+second line")) + }). + PressEnter() - input.Enter() - assert.CurrentView().Name("subCommits") - assert.MainView().Content(Contains("+second line")) - assert.CurrentView().SelectedLine(Contains("update")) - input.Enter() - assert.CurrentView().Name("commitFiles").SelectedLine(Contains("file1")) - assert.MainView().Content(Contains("+second line")) + t.Views().CommitFiles(). + IsFocused(). + SelectedLine(Contains("file1")). + Tap(func() { + t.Views().Main().Content(Contains("+second line")) + }). + PressEscape() - input.Press(keys.Universal.Return) - input.Press(keys.Universal.Return) - assert.CurrentView().Name("localBranches") + t.Views().SubCommits().PressEscape() - input.Press(keys.Universal.DiffingMenu) - input.Menu(Equals("Diffing"), Contains("reverse diff direction")) - assert.View("information").Content(Contains("showing output for: git diff branch-a branch-b -R")) - assert.MainView().Content(Contains("-second line")) + t.Views().Branches(). + IsFocused(). + Press(keys.Universal.DiffingMenu) + + t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains("reverse diff direction")).Confirm() + t.Views().Information().Content(Contains("showing output for: git diff branch-a branch-b -R")) + t.Views().Main().Content(Contains("-second line")) }, }) diff --git a/pkg/integration/tests/diff/diff_and_apply_patch.go b/pkg/integration/tests/diff/diff_and_apply_patch.go index e963bb296..8988b0493 100644 --- a/pkg/integration/tests/diff/diff_and_apply_patch.go +++ b/pkg/integration/tests/diff/diff_and_apply_patch.go @@ -21,48 +21,58 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{ shell.Checkout("branch-a") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToBranchesView() - assert.CurrentView().Lines( - Contains("branch-a"), - Contains("branch-b"), - ) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("branch-a"), + Contains("branch-b"), + ). + Press(keys.Universal.DiffingMenu) - input.Press(keys.Universal.DiffingMenu) + t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Equals("diff branch-a")).Confirm() - input.Menu(Equals("Diffing"), Equals("diff branch-a")) + t.Views().Information().Content(Contains("showing output for: git diff branch-a branch-a")) - assert.CurrentView().Name("localBranches") + t.Views().Branches(). + IsFocused(). + SelectNextItem(). + Tap(func() { + t.Views().Information().Content(Contains("showing output for: git diff branch-a branch-b")) + t.Views().Main().Content(Contains("+second line")) + }). + PressEnter() - assert.View("information").Content(Contains("showing output for: git diff branch-a branch-a")) - input.NextItem() - assert.View("information").Content(Contains("showing output for: git diff branch-a branch-b")) - assert.MainView().Content(Contains("+second line")) + t.Views().SubCommits(). + IsFocused(). + SelectedLine(Contains("update")). + Tap(func() { + t.Views().Main().Content(Contains("+second line")) + }). + PressEnter() - input.Enter() - assert.CurrentView().Name("subCommits") - assert.MainView().Content(Contains("+second line")) - assert.CurrentView().SelectedLine(Contains("update")) - input.Enter() - assert.CurrentView().Name("commitFiles") - assert.CurrentView().SelectedLine(Contains("file1")) - assert.MainView().Content(Contains("+second line")) + t.Views().CommitFiles(). + IsFocused(). + SelectedLine(Contains("file1")). + Tap(func() { + t.Views().Main().Content(Contains("+second line")) + }). + PressPrimaryAction(). // add the file to the patch + Press(keys.Universal.DiffingMenu). + Tap(func() { + t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains("exit diff mode")).Confirm() - // add the file to the patch - input.PrimaryAction() + t.Views().Information().Content(DoesNotContain("building patch")) + }). + Press(keys.Universal.CreatePatchOptionsMenu) - input.Press(keys.Universal.DiffingMenu) - input.Menu(Equals("Diffing"), Contains("exit diff mode")) - - 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 - input.Menu(Equals("Patch Options"), MatchesRegexp("apply patch$")) + t.ExpectPopup().Menu().Title(Equals("Patch Options")).Select(MatchesRegexp("apply patch$")).Confirm() - input.SwitchToFilesView() + t.Views().Files(). + Focus(). + SelectedLine(Contains("file1")) - assert.CurrentView().SelectedLine(Contains("file1")) - assert.MainView().Content(Contains("+second line")) + t.Views().Main().Content(Contains("+second line")) }, }) diff --git a/pkg/integration/tests/diff/diff_commits.go b/pkg/integration/tests/diff/diff_commits.go index efa44d818..6de643272 100644 --- a/pkg/integration/tests/diff/diff_commits.go +++ b/pkg/integration/tests/diff/diff_commits.go @@ -18,37 +18,38 @@ var DiffCommits = NewIntegrationTest(NewIntegrationTestArgs{ shell.UpdateFileAndAdd("file1", "first line\nsecond line\nthird line\n") shell.Commit("third commit") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToCommitsView() + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("third commit").IsSelected(), + Contains("second commit"), + Contains("first commit"), + ). + Press(keys.Universal.DiffingMenu). + Tap(func() { + t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(MatchesRegexp(`diff \w+`)).Confirm() - assert.CurrentView().Lines( - Contains("third commit"), - Contains("second commit"), - Contains("first commit"), - ) + t.Views().Information().Content(Contains("showing output for: git diff")) + }). + SelectNextItem(). + SelectNextItem(). + SelectedLine(Contains("first commit")). + Tap(func() { + t.Views().Main().Content(Contains("-second line\n-third line")) + }). + Press(keys.Universal.DiffingMenu). + Tap(func() { + t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains("reverse diff direction")).Confirm() - input.Press(keys.Universal.DiffingMenu) - input.Menu(Equals("Diffing"), MatchesRegexp(`diff \w+`)) + t.Views().Main().Content(Contains("+second line\n+third line")) + }). + PressEnter() - assert.NotInPopup() + t.Views().CommitFiles(). + IsFocused(). + SelectedLine(Contains("file1")) - assert.View("information").Content(Contains("showing output for: git diff")) - - input.NextItem() - input.NextItem() - assert.CurrentView().SelectedLine(Contains("first commit")) - - assert.MainView().Content(Contains("-second line\n-third line")) - - input.Press(keys.Universal.DiffingMenu) - input.Menu(Equals("Diffing"), Contains("reverse diff direction")) - assert.NotInPopup() - - assert.MainView().Content(Contains("+second line\n+third line")) - - input.Enter() - - assert.CurrentView().Name("commitFiles").SelectedLine(Contains("file1")) - assert.MainView().Content(Contains("+second line\n+third line")) + t.Views().Main().Content(Contains("+second line\n+third line")) }, }) diff --git a/pkg/integration/tests/file/dir_with_untracked_file.go b/pkg/integration/tests/file/dir_with_untracked_file.go index b85ea273b..39da6e157 100644 --- a/pkg/integration/tests/file/dir_with_untracked_file.go +++ b/pkg/integration/tests/file/dir_with_untracked_file.go @@ -21,10 +21,13 @@ var DirWithUntrackedFile = NewIntegrationTest(NewIntegrationTestArgs{ shell.CreateFile("dir/untracked", "bar") shell.UpdateFile("dir/file", "baz") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(1) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("first commit"), + ) - assert.MainView(). + t.Views().Main(). 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) diff --git a/pkg/integration/tests/file/discard_changes.go b/pkg/integration/tests/file/discard_changes.go index e44bb45d7..f3fc11a01 100644 --- a/pkg/integration/tests/file/discard_changes.go +++ b/pkg/integration/tests/file/discard_changes.go @@ -71,9 +71,7 @@ var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{ shell.RunShellCommand(`echo "renamed\nhaha" > renamed2.txt && git add renamed2.txt`) }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(3) - + Run: func(t *TestDriver, keys config.KeybindingConfig) { type statusFile struct { status string label string @@ -82,9 +80,12 @@ var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{ discardOneByOne := func(files []statusFile) { for _, file := range files { - assert.CurrentView().SelectedLine(Contains(file.status + " " + file.label)) - input.Press(keys.Universal.Remove) - input.Menu(Equals(file.menuTitle), Contains("discard all changes")) + t.Views().Files(). + IsFocused(). + SelectedLine(Contains(file.status + " " + file.label)). + Press(keys.Universal.Remove) + + t.ExpectPopup().Menu().Title(Equals(file.menuTitle)).Select(Contains("discard all changes")).Confirm() } } @@ -98,7 +99,10 @@ var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{ {status: "DU", label: "deleted-us.txt", menuTitle: "deleted-us.txt"}, }) - input.DenyConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?")) + t.ExpectPopup().Confirmation(). + Title(Equals("continue")). + Content(Contains("all merge conflicts resolved. Continue?")). + Cancel() discardOneByOne([]statusFile{ {status: "MD", label: "change-delete.txt", menuTitle: "change-delete.txt"}, @@ -115,6 +119,6 @@ var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{ {status: "??", label: "new.txt", menuTitle: "new.txt"}, }) - assert.WorkingTreeFileCount(0) + t.Views().Files().IsEmpty() }, }) diff --git a/pkg/integration/tests/interactive_rebase/amend_merge.go b/pkg/integration/tests/interactive_rebase/amend_merge.go index 449d5f614..7e5e64746 100644 --- a/pkg/integration/tests/interactive_rebase/amend_merge.go +++ b/pkg/integration/tests/interactive_rebase/amend_merge.go @@ -27,23 +27,35 @@ var AmendMerge = NewIntegrationTest(NewIntegrationTestArgs{ Merge("feature-branch"). CreateFileAndAdd(postMergeFilename, postMergeFileContent) }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(3) - - input.SwitchToCommitsView() - + Run: func(t *TestDriver, keys config.KeybindingConfig) { mergeCommitMessage := "Merge branch 'feature-branch' into development-branch" - assert.HeadCommitMessage(Contains(mergeCommitMessage)) - input.Press(keys.Commits.AmendToCommit) - input.AcceptConfirmation(Equals("Amend Commit"), Contains("Are you sure you want to amend this commit with your staged files?")) + t.Views().Commits(). + Lines( + Contains(mergeCommitMessage), + Contains("new feature commit"), + Contains("initial commit"), + ) + + t.Views().Commits(). + Focus(). + Press(keys.Commits.AmendToCommit) + + t.ExpectPopup().Confirmation(). + Title(Equals("Amend Commit")). + Content(Contains("Are you sure you want to amend this commit with your staged files?")). + Confirm() // assuring we haven't added a brand new commit - assert.CommitCount(3) - assert.HeadCommitMessage(Contains(mergeCommitMessage)) + t.Views().Commits(). + Lines( + Contains(mergeCommitMessage), + Contains("new feature commit"), + Contains("initial commit"), + ) // assuring the post-merge file shows up in the merge commit. - assert.MainView(). + t.Views().Main(). Content(Contains(postMergeFilename)). Content(Contains("++" + postMergeFileContent)) }, diff --git a/pkg/integration/tests/interactive_rebase/one.go b/pkg/integration/tests/interactive_rebase/one.go index 3c4c07a4d..d6d01239a 100644 --- a/pkg/integration/tests/interactive_rebase/one.go +++ b/pkg/integration/tests/interactive_rebase/one.go @@ -14,63 +14,58 @@ var One = NewIntegrationTest(NewIntegrationTestArgs{ shell. CreateNCommits(5) // these will appears at commit 05, 04, 04, down to 01 }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToCommitsView() - assert.CurrentView().Lines( - Contains("commit 05"), - Contains("commit 04"), - Contains("commit 03"), - Contains("commit 02"), - Contains("commit 01"), - ) - - input.NavigateToListItem(Contains("commit 02")) - input.Press(keys.Universal.Edit) - - assert.CurrentView().Lines( - MatchesRegexp("pick.*commit 05"), - MatchesRegexp("pick.*commit 04"), - MatchesRegexp("pick.*commit 03"), - MatchesRegexp("YOU ARE HERE.*commit 02"), - Contains("commit 01"), - ) - - input.PreviousItem() - input.Press(keys.Commits.MarkCommitAsFixup) - assert.CurrentView().Lines( - MatchesRegexp("pick.*commit 05"), - MatchesRegexp("pick.*commit 04"), - MatchesRegexp("fixup.*commit 03"), - MatchesRegexp("YOU ARE HERE.*commit 02"), - Contains("commit 01"), - ) - - input.PreviousItem() - input.Press(keys.Universal.Remove) - assert.CurrentView().Lines( - MatchesRegexp("pick.*commit 05"), - MatchesRegexp("drop.*commit 04"), - MatchesRegexp("fixup.*commit 03"), - MatchesRegexp("YOU ARE HERE.*commit 02"), - Contains("commit 01"), - ) - - input.PreviousItem() - input.Press(keys.Commits.SquashDown) - - assert.CurrentView().Lines( - MatchesRegexp("squash.*commit 05"), - MatchesRegexp("drop.*commit 04"), - MatchesRegexp("fixup.*commit 03"), - MatchesRegexp("YOU ARE HERE.*commit 02"), - Contains("commit 01"), - ) - - input.ContinueRebase() - - assert.CurrentView().Lines( - Contains("commit 02"), - Contains("commit 01"), - ) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("commit 05"), + Contains("commit 04"), + Contains("commit 03"), + Contains("commit 02"), + Contains("commit 01"), + ). + NavigateToListItem(Contains("commit 02")). + Press(keys.Universal.Edit). + Lines( + MatchesRegexp("pick.*commit 05"), + MatchesRegexp("pick.*commit 04"), + MatchesRegexp("pick.*commit 03"), + MatchesRegexp("YOU ARE HERE.*commit 02").IsSelected(), + Contains("commit 01"), + ). + SelectPreviousItem(). + Press(keys.Commits.MarkCommitAsFixup). + Lines( + MatchesRegexp("pick.*commit 05"), + MatchesRegexp("pick.*commit 04"), + MatchesRegexp("fixup.*commit 03").IsSelected(), + MatchesRegexp("YOU ARE HERE.*commit 02"), + Contains("commit 01"), + ). + SelectPreviousItem(). + Press(keys.Universal.Remove). + Lines( + MatchesRegexp("pick.*commit 05"), + MatchesRegexp("drop.*commit 04").IsSelected(), + MatchesRegexp("fixup.*commit 03"), + MatchesRegexp("YOU ARE HERE.*commit 02"), + Contains("commit 01"), + ). + SelectPreviousItem(). + Press(keys.Commits.SquashDown). + Lines( + MatchesRegexp("squash.*commit 05").IsSelected(), + MatchesRegexp("drop.*commit 04"), + MatchesRegexp("fixup.*commit 03"), + MatchesRegexp("YOU ARE HERE.*commit 02"), + Contains("commit 01"), + ). + Tap(func() { + t.Actions().ContinueRebase() + }). + Lines( + Contains("commit 02"), + Contains("commit 01"), + ) }, }) diff --git a/pkg/integration/tests/misc/confirm_on_quit.go b/pkg/integration/tests/misc/confirm_on_quit.go index 183f7dd0b..18b27a108 100644 --- a/pkg/integration/tests/misc/confirm_on_quit.go +++ b/pkg/integration/tests/misc/confirm_on_quit.go @@ -13,10 +13,14 @@ var ConfirmOnQuit = NewIntegrationTest(NewIntegrationTestArgs{ config.UserConfig.ConfirmOnQuit = true }, SetupRepo: func(shell *Shell) {}, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.CommitCount(0) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.Quit) - input.Press(keys.Universal.Quit) - input.AcceptConfirmation(Equals(""), Contains("Are you sure you want to quit?")) + t.ExpectPopup().Confirmation(). + Title(Equals("")). + Content(Contains("Are you sure you want to quit?")). + Confirm() }, }) diff --git a/pkg/integration/tests/stash/rename.go b/pkg/integration/tests/stash/rename.go index 7088de423..0b47bcef3 100644 --- a/pkg/integration/tests/stash/rename.go +++ b/pkg/integration/tests/stash/rename.go @@ -18,18 +18,18 @@ var Rename = NewIntegrationTest(NewIntegrationTestArgs{ CreateFileAndAdd("file-2", "change to stash2"). StashWithMessage("bar") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.SwitchToStashView() - - assert.CurrentView().Lines( - Equals("On master: bar"), - Equals("On master: foo"), - ) - input.NextItem() - input.Press(keys.Stash.RenameStash) - - input.Prompt(Equals("Rename stash: stash@{1}"), " baz") - - assert.CurrentView().SelectedLine(Equals("On master: foo baz")) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Stash(). + Focus(). + Lines( + Equals("On master: bar"), + Equals("On master: foo"), + ). + SelectNextItem(). + Press(keys.Stash.RenameStash). + Tap(func() { + t.ExpectPopup().Prompt().Title(Equals("Rename stash: stash@{1}")).Type(" baz").Confirm() + }). + SelectedLine(Equals("On master: foo baz")) }, }) diff --git a/pkg/integration/tests/stash/stash.go b/pkg/integration/tests/stash/stash.go index 23ff7e99d..f88aac2d0 100644 --- a/pkg/integration/tests/stash/stash.go +++ b/pkg/integration/tests/stash/stash.go @@ -15,17 +15,26 @@ var Stash = NewIntegrationTest(NewIntegrationTestArgs{ shell.CreateFile("file", "content") shell.GitAddAll() }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.StashCount(0) - assert.WorkingTreeFileCount(1) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Stash(). + IsEmpty() - input.Press(keys.Files.ViewStashOptions) + t.Views().Files(). + Lines( + Contains("file"), + ). + Press(keys.Files.ViewStashOptions) - input.Menu(Equals("Stash options"), MatchesRegexp("stash all changes$")) + t.ExpectPopup().Menu().Title(Equals("Stash options")).Select(MatchesRegexp("stash all changes$")).Confirm() - input.Prompt(Equals("Stash changes"), "my stashed file") + t.ExpectPopup().Prompt().Title(Equals("Stash changes")).Type("my stashed file").Confirm() - assert.StashCount(1) - assert.WorkingTreeFileCount(0) + t.Views().Stash(). + Lines( + Contains("my stashed file"), + ) + + t.Views().Files(). + IsEmpty() }, }) diff --git a/pkg/integration/tests/stash/stash_including_untracked_files.go b/pkg/integration/tests/stash/stash_including_untracked_files.go index d193600df..770c87b9c 100644 --- a/pkg/integration/tests/stash/stash_including_untracked_files.go +++ b/pkg/integration/tests/stash/stash_including_untracked_files.go @@ -16,17 +16,27 @@ var StashIncludingUntrackedFiles = NewIntegrationTest(NewIntegrationTestArgs{ shell.CreateFile("file_2", "content") shell.GitAdd("file_1") }, - Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - assert.StashCount(0) - assert.WorkingTreeFileCount(2) + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Stash(). + IsEmpty() - input.Press(keys.Files.ViewStashOptions) + t.Views().Files(). + Lines( + Contains("file_1"), + Contains("file_2"), + ). + Press(keys.Files.ViewStashOptions) - input.Menu(Equals("Stash options"), Contains("stash all changes including untracked files")) + t.ExpectPopup().Menu().Title(Equals("Stash options")).Select(Contains("stash all changes including untracked files")).Confirm() - input.Prompt(Equals("Stash changes"), "my stashed file") + t.ExpectPopup().Prompt().Title(Equals("Stash changes")).Type("my stashed file").Confirm() - assert.StashCount(1) - assert.WorkingTreeFileCount(0) + t.Views().Stash(). + Lines( + Contains("my stashed file"), + ) + + t.Views().Files(). + IsEmpty() }, }) diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 4bc8569f7..5326054d2 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -20,7 +20,6 @@ type GuiDriver interface { PressKey(string) Keys() config.KeybindingConfig CurrentContext() types.Context - Model() *types.Model Fail(message string) // These two log methods are for the sake of debugging while testing. There's no need to actually // commit any logging.