mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-10-08 22:52:12 +02:00
combine assert and input structs, clean up interface
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
package components
|
||||
|
||||
type AlertAsserter struct {
|
||||
assert *Assert
|
||||
input *Input
|
||||
hasCheckedTitle bool
|
||||
hasCheckedContent bool
|
||||
}
|
||||
|
||||
func (self *AlertAsserter) getViewAsserter() *View {
|
||||
return self.assert.Views().ByName("confirmation")
|
||||
return self.input.Views().Confirmation()
|
||||
}
|
||||
|
||||
// asserts that the alert view has the expected title
|
||||
@@ -32,17 +31,17 @@ func (self *AlertAsserter) Content(expected *matcher) *AlertAsserter {
|
||||
func (self *AlertAsserter) Confirm() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Confirm()
|
||||
self.getViewAsserter().PressEnter()
|
||||
}
|
||||
|
||||
func (self *AlertAsserter) Cancel() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Press(self.input.keys.Universal.Return)
|
||||
self.getViewAsserter().PressEscape()
|
||||
}
|
||||
|
||||
func (self *AlertAsserter) checkNecessaryChecksCompleted() {
|
||||
if !self.hasCheckedContent || !self.hasCheckedTitle {
|
||||
self.assert.Fail("You must both check the content and title of a confirmation popup by calling Title()/Content() before calling Confirm()/Cancel().")
|
||||
self.input.Fail("You must both check the content and title of a confirmation popup by calling Title()/Content() before calling Confirm()/Cancel().")
|
||||
}
|
||||
}
|
||||
|
@@ -1,38 +0,0 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// through this struct we assert on the state of the lazygit gui
|
||||
|
||||
type Assert struct {
|
||||
input *Input
|
||||
gui integrationTypes.GuiDriver
|
||||
*assertionHelper
|
||||
}
|
||||
|
||||
func NewAssert(gui integrationTypes.GuiDriver) *Assert {
|
||||
return &Assert{gui: gui}
|
||||
}
|
||||
|
||||
// for making assertions on lazygit views
|
||||
func (self *Assert) Views() *Views {
|
||||
return &Views{assert: self, input: self.input}
|
||||
}
|
||||
|
||||
// for making assertions on the lazygit model
|
||||
func (self *Assert) Model() *Model {
|
||||
return &Model{assertionHelper: self.assertionHelper, gui: self.gui}
|
||||
}
|
||||
|
||||
// for making assertions on the file system
|
||||
func (self *Assert) 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 *Assert) Fail(message string) {
|
||||
self.assertionHelper.fail(message)
|
||||
}
|
@@ -1,12 +1,11 @@
|
||||
package components
|
||||
|
||||
type CommitMessagePanelAsserter struct {
|
||||
assert *Assert
|
||||
input *Input
|
||||
input *Input
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelAsserter) getViewAsserter() *View {
|
||||
return self.assert.Views().ByName("commitMessage")
|
||||
return self.input.Views().CommitMessage()
|
||||
}
|
||||
|
||||
// asserts on the text initially present in the prompt
|
||||
@@ -17,13 +16,13 @@ func (self *CommitMessagePanelAsserter) InitialText(expected *matcher) *CommitMe
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelAsserter) Type(value string) *CommitMessagePanelAsserter {
|
||||
self.input.Type(value)
|
||||
self.input.typeContent(value)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelAsserter) AddNewline() *CommitMessagePanelAsserter {
|
||||
self.input.Press(self.input.keys.Universal.AppendNewline)
|
||||
self.input.press(self.input.keys.Universal.AppendNewline)
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -33,9 +32,9 @@ func (self *CommitMessagePanelAsserter) Clear() *CommitMessagePanelAsserter {
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelAsserter) Confirm() {
|
||||
self.input.Confirm()
|
||||
self.getViewAsserter().PressEnter()
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelAsserter) Cancel() {
|
||||
self.input.Press(self.input.keys.Universal.Return)
|
||||
self.getViewAsserter().PressEscape()
|
||||
}
|
||||
|
@@ -1,14 +1,13 @@
|
||||
package components
|
||||
|
||||
type ConfirmationAsserter struct {
|
||||
assert *Assert
|
||||
input *Input
|
||||
hasCheckedTitle bool
|
||||
hasCheckedContent bool
|
||||
}
|
||||
|
||||
func (self *ConfirmationAsserter) getViewAsserter() *View {
|
||||
return self.assert.Views().ByName("confirmation")
|
||||
return self.input.Views().Confirmation()
|
||||
}
|
||||
|
||||
// asserts that the confirmation view has the expected title
|
||||
@@ -32,17 +31,17 @@ func (self *ConfirmationAsserter) Content(expected *matcher) *ConfirmationAssert
|
||||
func (self *ConfirmationAsserter) Confirm() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Confirm()
|
||||
self.getViewAsserter().PressEnter()
|
||||
}
|
||||
|
||||
func (self *ConfirmationAsserter) Cancel() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Press(self.input.keys.Universal.Return)
|
||||
self.getViewAsserter().PressEscape()
|
||||
}
|
||||
|
||||
func (self *ConfirmationAsserter) checkNecessaryChecksCompleted() {
|
||||
if !self.hasCheckedContent || !self.hasCheckedTitle {
|
||||
self.assert.Fail("You must both check the content and title of a confirmation popup by calling Title()/Content() before calling Confirm()/Cancel().")
|
||||
self.input.Fail("You must both check the content and title of a confirmation popup by calling Title()/Content() before calling Confirm()/Cancel().")
|
||||
}
|
||||
}
|
||||
|
@@ -8,14 +8,14 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Input struct {
|
||||
gui integrationTypes.GuiDriver
|
||||
keys config.KeybindingConfig
|
||||
assert *Assert
|
||||
*assertionHelper
|
||||
gui integrationTypes.GuiDriver
|
||||
keys config.KeybindingConfig
|
||||
pushKeyDelay int
|
||||
*assertionHelper
|
||||
}
|
||||
|
||||
func NewInput(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, pushKeyDelay int) *Input {
|
||||
@@ -23,119 +23,31 @@ func NewInput(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, push
|
||||
gui: gui,
|
||||
keys: keys,
|
||||
pushKeyDelay: pushKeyDelay,
|
||||
assertionHelper: assert.assertionHelper,
|
||||
assertionHelper: &assertionHelper{gui: gui},
|
||||
}
|
||||
}
|
||||
|
||||
// key is something like 'w' or '<space>'. 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.currentWindowName("status")
|
||||
}
|
||||
|
||||
// switch to status window and assert that the status view is on top
|
||||
func (self *Input) SwitchToStatusView() {
|
||||
self.SwitchToStatusWindow()
|
||||
self.assert.Views().Current().Name("status")
|
||||
}
|
||||
|
||||
func (self *Input) switchToFilesWindow() {
|
||||
self.press(self.keys.Universal.JumpToBlock[1])
|
||||
self.currentWindowName("files")
|
||||
}
|
||||
|
||||
// switch to files window and assert that the files view is on top
|
||||
func (self *Input) SwitchToFilesView() {
|
||||
self.switchToFilesWindow()
|
||||
self.assert.Views().Current().Name("files")
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToBranchesWindow() {
|
||||
self.press(self.keys.Universal.JumpToBlock[2])
|
||||
self.currentWindowName("localBranches")
|
||||
}
|
||||
|
||||
// switch to branches window and assert that the branches view is on top
|
||||
func (self *Input) SwitchToBranchesView() {
|
||||
self.SwitchToBranchesWindow()
|
||||
self.assert.Views().Current().Name("localBranches")
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToCommitsWindow() {
|
||||
self.press(self.keys.Universal.JumpToBlock[3])
|
||||
self.currentWindowName("commits")
|
||||
}
|
||||
|
||||
// switch to commits window and assert that the commits view is on top
|
||||
func (self *Input) SwitchToCommitsView() {
|
||||
self.SwitchToCommitsWindow()
|
||||
self.assert.Views().Current().Name("commits")
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToStashWindow() {
|
||||
self.press(self.keys.Universal.JumpToBlock[4])
|
||||
self.currentWindowName("stash")
|
||||
}
|
||||
|
||||
// switch to stash window and assert that the stash view is on top
|
||||
func (self *Input) SwitchToStashView() {
|
||||
self.SwitchToStashWindow()
|
||||
self.assert.Views().Current().Name("stash")
|
||||
}
|
||||
|
||||
func (self *Input) Type(content string) {
|
||||
func (self *Input) typeContent(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.Views().Current().SelectedLine(Contains("continue"))
|
||||
self.Confirm()
|
||||
self.Views().current().Press(self.keys.Universal.CreateRebaseOptionsMenu)
|
||||
|
||||
self.ExpectMenu().
|
||||
Title(Equals("Rebase Options")).
|
||||
Select(Contains("continue")).
|
||||
Confirm()
|
||||
}
|
||||
|
||||
func (self *Input) ContinueRebase() {
|
||||
@@ -166,7 +78,7 @@ func (self *Input) Log(message string) {
|
||||
// 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) {
|
||||
func (self *Input) navigateToListItem(matcher *matcher) {
|
||||
self.inListContext()
|
||||
|
||||
currentContext := self.gui.CurrentContext().(types.IListContext)
|
||||
@@ -175,7 +87,7 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
|
||||
|
||||
var matchIndex int
|
||||
|
||||
self.assert.assertWithRetries(func() (bool, string) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
matchIndex = -1
|
||||
var matches []string
|
||||
lines := view.ViewBufferLines()
|
||||
@@ -198,20 +110,20 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
|
||||
|
||||
selectedLineIdx := view.SelectedLineIdx()
|
||||
if selectedLineIdx == matchIndex {
|
||||
self.assert.Views().Current().SelectedLine(matcher)
|
||||
self.Views().current().SelectedLine(matcher)
|
||||
return
|
||||
}
|
||||
if selectedLineIdx < matchIndex {
|
||||
for i := selectedLineIdx; i < matchIndex; i++ {
|
||||
self.NextItem()
|
||||
self.Views().current().SelectNextItem()
|
||||
}
|
||||
self.assert.Views().Current().SelectedLine(matcher)
|
||||
self.Views().current().SelectedLine(matcher)
|
||||
return
|
||||
} else {
|
||||
for i := selectedLineIdx; i > matchIndex; i-- {
|
||||
self.PreviousItem()
|
||||
self.Views().current().SelectPreviousItem()
|
||||
}
|
||||
self.assert.Views().Current().SelectedLine(matcher)
|
||||
self.Views().current().SelectedLine(matcher)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -224,10 +136,10 @@ func (self *Input) inListContext() {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Input) Confirmation() *ConfirmationAsserter {
|
||||
func (self *Input) ExpectConfirmation() *ConfirmationAsserter {
|
||||
self.inConfirm()
|
||||
|
||||
return &ConfirmationAsserter{assert: self.assert, input: self}
|
||||
return &ConfirmationAsserter{input: self}
|
||||
}
|
||||
|
||||
func (self *Input) inConfirm() {
|
||||
@@ -237,10 +149,10 @@ func (self *Input) inConfirm() {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Input) Prompt() *PromptAsserter {
|
||||
func (self *Input) ExpectPrompt() *PromptAsserter {
|
||||
self.inPrompt()
|
||||
|
||||
return &PromptAsserter{assert: self.assert, input: self}
|
||||
return &PromptAsserter{input: self}
|
||||
}
|
||||
|
||||
func (self *Input) inPrompt() {
|
||||
@@ -250,10 +162,10 @@ func (self *Input) inPrompt() {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Input) Alert() *AlertAsserter {
|
||||
func (self *Input) ExpectAlert() *AlertAsserter {
|
||||
self.inAlert()
|
||||
|
||||
return &AlertAsserter{assert: self.assert, input: self}
|
||||
return &AlertAsserter{input: self}
|
||||
}
|
||||
|
||||
func (self *Input) inAlert() {
|
||||
@@ -264,10 +176,10 @@ func (self *Input) inAlert() {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Input) Menu() *MenuAsserter {
|
||||
func (self *Input) ExpectMenu() *MenuAsserter {
|
||||
self.inMenu()
|
||||
|
||||
return &MenuAsserter{assert: self.assert, input: self}
|
||||
return &MenuAsserter{input: self}
|
||||
}
|
||||
|
||||
func (self *Input) inMenu() {
|
||||
@@ -276,10 +188,10 @@ func (self *Input) inMenu() {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Input) CommitMessagePanel() *CommitMessagePanelAsserter {
|
||||
func (self *Input) ExpectCommitMessagePanel() *CommitMessagePanelAsserter {
|
||||
self.inCommitMessagePanel()
|
||||
|
||||
return &CommitMessagePanelAsserter{assert: self.assert, input: self}
|
||||
return &CommitMessagePanelAsserter{input: self}
|
||||
}
|
||||
|
||||
func (self *Input) inCommitMessagePanel() {
|
||||
@@ -295,3 +207,31 @@ func (self *Input) currentWindowName(expectedWindowName string) {
|
||||
return actual == expectedWindowName, fmt.Sprintf("Expected current window name to be '%s', but got '%s'", expectedWindowName, actual)
|
||||
})
|
||||
}
|
||||
|
||||
// for making assertions on lazygit views
|
||||
func (self *Input) Views() *Views {
|
||||
return &Views{input: self}
|
||||
}
|
||||
|
||||
// for making assertions on the lazygit model
|
||||
func (self *Input) Model() *Model {
|
||||
return &Model{assertionHelper: self.assertionHelper, gui: self.gui}
|
||||
}
|
||||
|
||||
// for making assertions on the file system
|
||||
func (self *Input) 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 *Input) Fail(message string) {
|
||||
self.assertionHelper.fail(message)
|
||||
}
|
||||
|
||||
func (self *Input) NotInPopup() {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
viewName := self.gui.CurrentContext().GetView().Name()
|
||||
return !lo.Contains([]string{"menu", "confirmation", "commitMessage"}, viewName), fmt.Sprintf("Unexpected popup view present: %s view", viewName)
|
||||
})
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
package components
|
||||
|
||||
type MenuAsserter struct {
|
||||
assert *Assert
|
||||
input *Input
|
||||
hasCheckedTitle bool
|
||||
}
|
||||
|
||||
func (self *MenuAsserter) getViewAsserter() *View {
|
||||
return self.assert.Views().ByName("menu")
|
||||
return self.input.Views().Menu()
|
||||
}
|
||||
|
||||
// asserts that the popup has the expected title
|
||||
@@ -22,23 +21,23 @@ func (self *MenuAsserter) Title(expected *matcher) *MenuAsserter {
|
||||
func (self *MenuAsserter) Confirm() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Confirm()
|
||||
self.getViewAsserter().PressEnter()
|
||||
}
|
||||
|
||||
func (self *MenuAsserter) Cancel() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Press(self.input.keys.Universal.Return)
|
||||
self.getViewAsserter().PressEscape()
|
||||
}
|
||||
|
||||
func (self *MenuAsserter) Select(option *matcher) *MenuAsserter {
|
||||
self.input.NavigateToListItem(option)
|
||||
self.getViewAsserter().NavigateToListItem(option)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuAsserter) checkNecessaryChecksCompleted() {
|
||||
if !self.hasCheckedTitle {
|
||||
self.assert.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().")
|
||||
self.input.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().")
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ type Model struct {
|
||||
gui integrationTypes.GuiDriver
|
||||
}
|
||||
|
||||
func (self *Model) WorkingTreeFileCount(expectedCount int) {
|
||||
func (self *Model) WorkingTreeFileCount(expectedCount int) *Model {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Files)
|
||||
|
||||
@@ -20,9 +20,11 @@ func (self *Model) WorkingTreeFileCount(expectedCount int) {
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Model) CommitCount(expectedCount int) {
|
||||
func (self *Model) CommitCount(expectedCount int) *Model {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Commits)
|
||||
|
||||
@@ -31,9 +33,11 @@ func (self *Model) CommitCount(expectedCount int) {
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Model) StashCount(expectedCount int) {
|
||||
func (self *Model) StashCount(expectedCount int) *Model {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().StashEntries)
|
||||
|
||||
@@ -42,17 +46,21 @@ func (self *Model) StashCount(expectedCount int) {
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Model) AtLeastOneCommit() {
|
||||
func (self *Model) AtLeastOneCommit() *Model {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Commits)
|
||||
|
||||
return actualCount > 0, "Expected at least one commit present"
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Model) HeadCommitMessage(matcher *matcher) {
|
||||
func (self *Model) HeadCommitMessage(matcher *matcher) *Model {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
return len(self.gui.Model().Commits) > 0, "Expected at least one commit to be present"
|
||||
})
|
||||
@@ -62,11 +70,15 @@ func (self *Model) HeadCommitMessage(matcher *matcher) {
|
||||
return self.gui.Model().Commits[0].Name
|
||||
},
|
||||
)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Model) CurrentBranchName(expectedViewName string) {
|
||||
func (self *Model) CurrentBranchName(expectedViewName string) *Model {
|
||||
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)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
package components
|
||||
|
||||
type PromptAsserter struct {
|
||||
assert *Assert
|
||||
input *Input
|
||||
hasCheckedTitle bool
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) getViewAsserter() *View {
|
||||
return self.assert.Views().ByName("confirmation")
|
||||
return self.input.Views().Confirmation()
|
||||
}
|
||||
|
||||
// asserts that the popup has the expected title
|
||||
@@ -27,7 +26,7 @@ func (self *PromptAsserter) InitialText(expected *matcher) *PromptAsserter {
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) Type(value string) *PromptAsserter {
|
||||
self.input.Type(value)
|
||||
self.input.typeContent(value)
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -39,45 +38,47 @@ func (self *PromptAsserter) Clear() *PromptAsserter {
|
||||
func (self *PromptAsserter) Confirm() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Confirm()
|
||||
self.getViewAsserter().PressEnter()
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) Cancel() {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.input.Press(self.input.keys.Universal.Return)
|
||||
self.getViewAsserter().PressEscape()
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) checkNecessaryChecksCompleted() {
|
||||
if !self.hasCheckedTitle {
|
||||
self.assert.Fail("You must check the title of a prompt popup by calling Title() before calling Confirm()/Cancel().")
|
||||
self.input.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.assert.Views().ByName("suggestions").Lines(matchers...)
|
||||
self.input.Views().Suggestions().Lines(matchers...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) SuggestionTopLines(matchers ...*matcher) *PromptAsserter {
|
||||
self.assert.Views().ByName("suggestions").TopLines(matchers...)
|
||||
self.input.Views().Suggestions().TopLines(matchers...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) SelectFirstSuggestion() *PromptAsserter {
|
||||
self.input.Press(self.input.keys.Universal.TogglePanel)
|
||||
self.assert.Views().Current().Name("suggestions")
|
||||
self.input.press(self.input.keys.Universal.TogglePanel)
|
||||
self.input.Views().Suggestions().
|
||||
IsFocused().
|
||||
SelectedLineIdx(0)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *PromptAsserter) SelectSuggestion(matcher *matcher) *PromptAsserter {
|
||||
self.input.Press(self.input.keys.Universal.TogglePanel)
|
||||
self.assert.Views().Current().Name("suggestions")
|
||||
|
||||
self.input.NavigateToListItem(matcher)
|
||||
self.input.press(self.input.keys.Universal.TogglePanel)
|
||||
self.input.Views().Suggestions().
|
||||
IsFocused().
|
||||
NavigateToListItem(matcher)
|
||||
|
||||
return self
|
||||
}
|
||||
|
@@ -26,7 +26,6 @@ type IntegrationTest struct {
|
||||
run func(
|
||||
shell *Shell,
|
||||
input *Input,
|
||||
assert *Assert,
|
||||
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(shell *Shell, input *Input, keys config.KeybindingConfig)
|
||||
// additional args passed to lazygit
|
||||
ExtraCmdArgs string
|
||||
// for when a test is flakey
|
||||
@@ -94,11 +93,10 @@ func (self *IntegrationTest) SetupRepo(shell *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())
|
||||
input := NewInput(gui, keys, KeyPressDelay())
|
||||
|
||||
self.run(shell, input, assert, keys)
|
||||
self.run(shell, input, keys)
|
||||
|
||||
if KeyPressDelay() > 0 {
|
||||
// the dev would want to see the final state if they're running in slow mode
|
||||
|
@@ -63,10 +63,10 @@ func (self *fakeGuiDriver) View(viewName string) *gocui.View {
|
||||
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.Model().CommitCount(2)
|
||||
Run: func(shell *Shell, input *Input, keys config.KeybindingConfig) {
|
||||
input.press("a")
|
||||
input.press("b")
|
||||
input.Model().CommitCount(2)
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
@@ -78,8 +78,8 @@ func TestAssertionFailure(t *testing.T) {
|
||||
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(shell *Shell, input *Input, keys config.KeybindingConfig) {
|
||||
input.Fail("blah")
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
@@ -90,10 +90,10 @@ 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.Model().CommitCount(0)
|
||||
Run: func(shell *Shell, input *Input, keys config.KeybindingConfig) {
|
||||
input.press("a")
|
||||
input.press("b")
|
||||
input.Model().CommitCount(0)
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
|
196
pkg/integration/components/view.go
Normal file
196
pkg/integration/components/view.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
input *Input
|
||||
}
|
||||
|
||||
// asserts that the view has the expected name. This is typically used in tandem with the CurrentView method i.e.;
|
||||
// input.CurrentView().Name("commits") to assert that the current view is the commits view.
|
||||
func (self *View) Name(expected string) *View {
|
||||
self.input.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 *View) Title(expected *matcher) *View {
|
||||
self.input.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 {
|
||||
self.input.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.input.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 *View) assertLines(matchers ...*matcher) *View {
|
||||
view := self.getView()
|
||||
|
||||
for i, matcher := range matchers {
|
||||
checkIsSelected, matcher := matcher.checkIsSelected()
|
||||
|
||||
self.input.matchString(matcher, fmt.Sprintf("Unexpected content in view '%s'.", view.Name()),
|
||||
func() string {
|
||||
return view.BufferLines()[i]
|
||||
},
|
||||
)
|
||||
|
||||
if checkIsSelected {
|
||||
self.input.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.input.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.input.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.input.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.input.fail(fmt.Sprintf("Cannot focus view %s: Focus() method not implemented", viewName))
|
||||
}
|
||||
|
||||
self.input.press(self.input.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.input.assertWithRetries(func() (bool, string) {
|
||||
expected := self.getView().Name()
|
||||
actual := self.input.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.input.press(keyStr)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// i.e. pressing down arrow
|
||||
func (self *View) SelectNextItem() *View {
|
||||
return self.Press(self.input.keys.Universal.NextItem)
|
||||
}
|
||||
|
||||
// i.e. pressing up arrow
|
||||
func (self *View) SelectPreviousItem() *View {
|
||||
return self.Press(self.input.keys.Universal.PrevItem)
|
||||
}
|
||||
|
||||
// i.e. pressing space
|
||||
func (self *View) PressPrimaryAction() *View {
|
||||
return self.Press(self.input.keys.Universal.Select)
|
||||
}
|
||||
|
||||
// i.e. pressing space
|
||||
func (self *View) PressEnter() *View {
|
||||
return self.Press(self.input.keys.Universal.Confirm)
|
||||
}
|
||||
|
||||
// i.e. pressing escape
|
||||
func (self *View) PressEscape() *View {
|
||||
return self.Press(self.input.keys.Universal.Return)
|
||||
}
|
||||
|
||||
func (self *View) NavigateToListItem(matcher *matcher) *View {
|
||||
self.IsFocused()
|
||||
|
||||
self.input.navigateToListItem(matcher)
|
||||
|
||||
return self
|
||||
}
|
@@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type Views struct {
|
||||
assert *Assert
|
||||
input *Input
|
||||
input *Input
|
||||
}
|
||||
|
||||
func (self *Views) Current() *View {
|
||||
// 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.assert.gui.CurrentContext().GetView() },
|
||||
assert: self.assert,
|
||||
getView: func() *gocui.View { return self.input.gui.CurrentContext().GetView() },
|
||||
input: self.input,
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,7 @@ func (self *Views) Current() *View {
|
||||
func (self *Views) Main() *View {
|
||||
return &View{
|
||||
context: "main view",
|
||||
getView: func() *gocui.View { return self.assert.gui.MainView() },
|
||||
assert: self.assert,
|
||||
getView: func() *gocui.View { return self.input.gui.MainView() },
|
||||
input: self.input,
|
||||
}
|
||||
}
|
||||
@@ -32,8 +31,7 @@ func (self *Views) Main() *View {
|
||||
func (self *Views) Secondary() *View {
|
||||
return &View{
|
||||
context: "secondary view",
|
||||
getView: func() *gocui.View { return self.assert.gui.SecondaryView() },
|
||||
assert: self.assert,
|
||||
getView: func() *gocui.View { return self.input.gui.SecondaryView() },
|
||||
input: self.input,
|
||||
}
|
||||
}
|
||||
@@ -41,143 +39,83 @@ func (self *Views) Secondary() *View {
|
||||
func (self *Views) ByName(viewName string) *View {
|
||||
return &View{
|
||||
context: fmt.Sprintf("%s view", viewName),
|
||||
getView: func() *gocui.View { return self.assert.gui.View(viewName) },
|
||||
assert: self.assert,
|
||||
getView: func() *gocui.View { return self.input.gui.View(viewName) },
|
||||
input: self.input,
|
||||
}
|
||||
}
|
||||
|
||||
type View struct {
|
||||
// context is prepended to any error messages e.g. 'context: "current view"'
|
||||
context string
|
||||
getView func() *gocui.View
|
||||
assert *Assert
|
||||
input *Input
|
||||
func (self *Views) Commits() *View {
|
||||
return self.ByName("commits")
|
||||
}
|
||||
|
||||
// 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 *View) Name(expected string) *View {
|
||||
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
|
||||
func (self *Views) Files() *View {
|
||||
return self.ByName("files")
|
||||
}
|
||||
|
||||
// asserts that the view has the expected title
|
||||
func (self *View) Title(expected *matcher) *View {
|
||||
self.assert.assertWithRetries(func() (bool, string) {
|
||||
actual := self.getView().Title
|
||||
return expected.context(fmt.Sprintf("%s title", self.context)).test(actual)
|
||||
})
|
||||
|
||||
return self
|
||||
func (self *Views) Status() *View {
|
||||
return self.ByName("status")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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...)
|
||||
func (self *Views) Submodules() *View {
|
||||
return self.ByName("submodules")
|
||||
}
|
||||
|
||||
// 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.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 *Views) Information() *View {
|
||||
return self.ByName("information")
|
||||
}
|
||||
|
||||
func (self *View) assertLines(matchers ...*matcher) *View {
|
||||
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
|
||||
func (self *Views) Branches() *View {
|
||||
return self.ByName("localBranches")
|
||||
}
|
||||
|
||||
// asserts on the content of the view i.e. the stuff within the view's frame.
|
||||
func (self *View) Content(matcher *matcher) *View {
|
||||
self.assert.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context),
|
||||
func() string {
|
||||
return self.getView().Buffer()
|
||||
},
|
||||
)
|
||||
|
||||
return self
|
||||
func (self *Views) RemoteBranches() *View {
|
||||
return self.ByName("remoteBranches")
|
||||
}
|
||||
|
||||
// asserts on the selected line of the view
|
||||
func (self *View) SelectedLine(matcher *matcher) *View {
|
||||
self.assert.matchString(matcher, fmt.Sprintf("%s: Unexpected selected line.", self.context),
|
||||
func() string {
|
||||
return self.getView().SelectedLine()
|
||||
},
|
||||
)
|
||||
|
||||
return self
|
||||
func (self *Views) Tags() *View {
|
||||
return self.ByName("tags")
|
||||
}
|
||||
|
||||
// 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.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
|
||||
func (self *Views) ReflogCommits() *View {
|
||||
return self.ByName("reflogCommits")
|
||||
}
|
||||
|
||||
// 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).
|
||||
// whitelistedViewNames := []string{"status", "files", "localBranches", "commits", "stash"}
|
||||
func (self *Views) SubCommits() *View {
|
||||
return self.ByName("subCommits")
|
||||
}
|
||||
|
||||
// viewName := self.getView().Name()
|
||||
func (self *Views) CommitFiles() *View {
|
||||
return self.ByName("commitFiles")
|
||||
}
|
||||
|
||||
// if !lo.Contains(whitelistedViewNames, self.getView().Name()) {
|
||||
// self.assert.fail(fmt.Sprintf("Cannot focus view %s: Focus() method not implemented", viewName))
|
||||
// }
|
||||
func (self *Views) Stash() *View {
|
||||
return self.ByName("stash")
|
||||
}
|
||||
|
||||
// windowIndexMap := map[string]int{
|
||||
// "status": 0,
|
||||
// "files": 1,
|
||||
// "localBranches": 2,
|
||||
// "commits": 3,
|
||||
// "stash": 4,
|
||||
// }
|
||||
func (self *Views) Staging() *View {
|
||||
return self.ByName("staging")
|
||||
}
|
||||
|
||||
// self.input.switchToFilesWindow()
|
||||
// self.press(self.keys.Universal.JumpToBlock[1])
|
||||
// self.assert.Views().Current().Name(viewName)
|
||||
func (self *Views) StagingSecondary() *View {
|
||||
return self.ByName("stagingSecondary")
|
||||
}
|
||||
|
||||
// return self
|
||||
// }
|
||||
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")
|
||||
}
|
||||
|
Reference in New Issue
Block a user