1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-10-08 22:52:12 +02:00

better namespacing for assertions

This commit is contained in:
Jesse Duffield
2022-12-27 15:22:31 +11:00
parent be30cbb375
commit 09e80e5f2a
36 changed files with 328 additions and 294 deletions

View File

@@ -7,7 +7,7 @@ type AlertAsserter struct {
hasCheckedContent bool
}
func (self *AlertAsserter) getViewAsserter() *ViewAsserter {
func (self *AlertAsserter) getViewAsserter() *Views {
return self.assert.Views().ByName("confirmation")
}

View File

@@ -1,12 +1,6 @@
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"
)
@@ -14,212 +8,30 @@ import (
type Assert struct {
gui integrationTypes.GuiDriver
*assertionHelper
}
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)
})
}
// for making assertions on lazygit views
func (self *Assert) Views() *ViewAsserterGetter {
return &ViewAsserterGetter{
assert: self,
}
return &ViewAsserterGetter{assert: self}
}
type ViewAsserterGetter struct {
assert *Assert
// for making assertions on the lazygit model
func (self *Assert) Model() *Model {
return &Model{assertionHelper: self.assertionHelper, gui: self.gui}
}
func (self *ViewAsserterGetter) Current() *ViewAsserter {
return &ViewAsserter{
context: "current view",
getView: func() *gocui.View { return self.assert.gui.CurrentContext().GetView() },
assert: self.assert,
}
// for making assertions on the file system
func (self *Assert) FileSystem() *FileSystem {
return &FileSystem{assertionHelper: self.assertionHelper}
}
func (self *ViewAsserterGetter) Main() *ViewAsserter {
return &ViewAsserter{
context: "main view",
getView: func() *gocui.View { return self.assert.gui.MainView() },
assert: self.assert,
}
}
func (self *ViewAsserterGetter) Secondary() *ViewAsserter {
return &ViewAsserter{
context: "secondary view",
getView: func() *gocui.View { return self.assert.gui.SecondaryView() },
assert: self.assert,
}
}
func (self *ViewAsserterGetter) ByName(viewName string) *ViewAsserter {
return &ViewAsserter{
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.assert.gui.View(viewName) },
assert: self.assert,
}
// 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)
}

View File

@@ -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)
}

View File

@@ -5,7 +5,7 @@ type CommitMessagePanelAsserter struct {
input *Input
}
func (self *CommitMessagePanelAsserter) getViewAsserter() *ViewAsserter {
func (self *CommitMessagePanelAsserter) getViewAsserter() *Views {
return self.assert.Views().ByName("commitMessage")
}

View File

@@ -7,7 +7,7 @@ type ConfirmationAsserter struct {
hasCheckedContent bool
}
func (self *ConfirmationAsserter) getViewAsserter() *ViewAsserter {
func (self *ConfirmationAsserter) getViewAsserter() *Views {
return self.assert.Views().ByName("confirmation")
}

View File

@@ -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)
})
}

View File

@@ -11,18 +11,20 @@ import (
)
type Input struct {
gui integrationTypes.GuiDriver
keys config.KeybindingConfig
assert *Assert
gui integrationTypes.GuiDriver
keys config.KeybindingConfig
assert *Assert
*assertionHelper
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,
gui: gui,
keys: keys,
assert: assert,
pushKeyDelay: pushKeyDelay,
assertionHelper: assert.assertionHelper,
}
}
@@ -42,7 +44,7 @@ func (self *Input) press(keyStr string) {
func (self *Input) SwitchToStatusWindow() {
self.press(self.keys.Universal.JumpToBlock[0])
self.assert.CurrentWindowName("status")
self.currentWindowName("status")
}
// switch to status window and assert that the status view is on top
@@ -53,7 +55,7 @@ func (self *Input) SwitchToStatusView() {
func (self *Input) SwitchToFilesWindow() {
self.press(self.keys.Universal.JumpToBlock[1])
self.assert.CurrentWindowName("files")
self.currentWindowName("files")
}
// switch to files window and assert that the files view is on top
@@ -64,7 +66,7 @@ func (self *Input) SwitchToFilesView() {
func (self *Input) SwitchToBranchesWindow() {
self.press(self.keys.Universal.JumpToBlock[2])
self.assert.CurrentWindowName("localBranches")
self.currentWindowName("localBranches")
}
// switch to branches window and assert that the branches view is on top
@@ -75,7 +77,7 @@ func (self *Input) SwitchToBranchesView() {
func (self *Input) SwitchToCommitsWindow() {
self.press(self.keys.Universal.JumpToBlock[3])
self.assert.CurrentWindowName("commits")
self.currentWindowName("commits")
}
// switch to commits window and assert that the commits view is on top
@@ -86,7 +88,7 @@ func (self *Input) SwitchToCommitsView() {
func (self *Input) SwitchToStashWindow() {
self.press(self.keys.Universal.JumpToBlock[4])
self.assert.CurrentWindowName("stash")
self.currentWindowName("stash")
}
// switch to stash window and assert that the stash view is on top
@@ -166,7 +168,7 @@ func (self *Input) Log(message string) {
// 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()
self.inListContext()
currentContext := self.gui.CurrentContext().(types.IListContext)
@@ -215,32 +217,82 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
}
}
func (self *Input) 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 *Input) Confirmation() *ConfirmationAsserter {
self.assert.InConfirm()
self.inConfirm()
return &ConfirmationAsserter{assert: self.assert, input: self}
}
func (self *Input) 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 *Input) Prompt() *PromptAsserter {
self.assert.InPrompt()
self.inPrompt()
return &PromptAsserter{assert: self.assert, input: self}
}
func (self *Input) 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 *Input) Alert() *AlertAsserter {
self.assert.InAlert()
self.inAlert()
return &AlertAsserter{assert: self.assert, input: self}
}
func (self *Input) 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 *Input) Menu() *MenuAsserter {
self.assert.InMenu()
self.inMenu()
return &MenuAsserter{assert: self.assert, input: self}
}
func (self *Input) inMenu() {
self.assertWithRetries(func() (bool, string) {
return self.gui.CurrentContext().GetView().Name() == "menu", "Expected popup menu to be focused"
})
}
func (self *Input) CommitMessagePanel() *CommitMessagePanelAsserter {
self.assert.InCommitMessagePanel()
self.inCommitMessagePanel()
return &CommitMessagePanelAsserter{assert: self.assert, input: self}
}
func (self *Input) inCommitMessagePanel() {
self.assertWithRetries(func() (bool, string) {
currentView := self.gui.CurrentContext().GetView()
return currentView.Name() == "commitMessage", "Expected commit message panel to be focused"
})
}
func (self *Input) 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)
})
}

View File

@@ -6,7 +6,7 @@ type MenuAsserter struct {
hasCheckedTitle bool
}
func (self *MenuAsserter) getViewAsserter() *ViewAsserter {
func (self *MenuAsserter) getViewAsserter() *Views {
return self.assert.Views().ByName("menu")
}

View File

@@ -0,0 +1,72 @@
package components
import (
"fmt"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
type Model struct {
*assertionHelper
gui integrationTypes.GuiDriver
}
func (self *Model) 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 *Model) 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 *Model) 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 *Model) AtLeastOneCommit() {
self.assertWithRetries(func() (bool, string) {
actualCount := len(self.gui.Model().Commits)
return actualCount > 0, "Expected at least one commit present"
})
}
func (self *Model) 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 *Model) 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)
})
}

View File

@@ -6,7 +6,7 @@ type PromptAsserter struct {
hasCheckedTitle bool
}
func (self *PromptAsserter) getViewAsserter() *ViewAsserter {
func (self *PromptAsserter) getViewAsserter() *Views {
return self.assert.Views().ByName("confirmation")
}

View File

@@ -66,7 +66,7 @@ func TestAssertionFailure(t *testing.T) {
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
input.Press("a")
input.Press("b")
assert.CommitCount(2)
assert.Model().CommitCount(2)
},
})
driver := &fakeGuiDriver{}
@@ -93,7 +93,7 @@ func TestSuccess(t *testing.T) {
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
input.Press("a")
input.Press("b")
assert.CommitCount(0)
assert.Model().CommitCount(0)
},
})
driver := &fakeGuiDriver{}

View File

@@ -6,7 +6,7 @@ import (
"github.com/jesseduffield/gocui"
)
type ViewAsserter struct {
type Views struct {
// context is prepended to any error messages e.g. 'context: "current view"'
context string
getView func() *gocui.View
@@ -15,7 +15,7 @@ type ViewAsserter struct {
// 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 {
func (self *Views) Name(expected string) *Views {
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)
@@ -25,7 +25,7 @@ func (self *ViewAsserter) Name(expected string) *ViewAsserter {
}
// asserts that the view has the expected title
func (self *ViewAsserter) Title(expected *matcher) *ViewAsserter {
func (self *Views) Title(expected *matcher) *Views {
self.assert.assertWithRetries(func() (bool, string) {
actual := self.getView().Title
return expected.context(fmt.Sprintf("%s title", self.context)).test(actual)
@@ -38,7 +38,7 @@ func (self *ViewAsserter) Title(expected *matcher) *ViewAsserter {
// 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 {
func (self *Views) TopLines(matchers ...*matcher) *Views {
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))
@@ -49,7 +49,7 @@ func (self *ViewAsserter) TopLines(matchers ...*matcher) *ViewAsserter {
// 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 {
func (self *Views) Lines(matchers ...*matcher) *Views {
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))
@@ -58,7 +58,7 @@ func (self *ViewAsserter) Lines(matchers ...*matcher) *ViewAsserter {
return self.assertLines(matchers...)
}
func (self *ViewAsserter) assertLines(matchers ...*matcher) *ViewAsserter {
func (self *Views) assertLines(matchers ...*matcher) *Views {
view := self.getView()
for i, matcher := range matchers {
@@ -82,7 +82,7 @@ func (self *ViewAsserter) assertLines(matchers ...*matcher) *ViewAsserter {
}
// asserts on the content of the view i.e. the stuff within the view's frame.
func (self *ViewAsserter) Content(matcher *matcher) *ViewAsserter {
func (self *Views) Content(matcher *matcher) *Views {
self.assert.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context),
func() string {
return self.getView().Buffer()
@@ -93,7 +93,7 @@ func (self *ViewAsserter) Content(matcher *matcher) *ViewAsserter {
}
// asserts on the selected line of the view
func (self *ViewAsserter) SelectedLine(matcher *matcher) *ViewAsserter {
func (self *Views) SelectedLine(matcher *matcher) *Views {
self.assert.matchString(matcher, fmt.Sprintf("%s: Unexpected selected line.", self.context),
func() string {
return self.getView().SelectedLine()
@@ -104,7 +104,7 @@ func (self *ViewAsserter) SelectedLine(matcher *matcher) *ViewAsserter {
}
// 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 {
func (self *Views) SelectedLineIdx(expected int) *Views {
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)
@@ -112,3 +112,39 @@ func (self *ViewAsserter) SelectedLineIdx(expected int) *ViewAsserter {
return self
}
type ViewAsserterGetter struct {
assert *Assert
}
func (self *ViewAsserterGetter) Current() *Views {
return &Views{
context: "current view",
getView: func() *gocui.View { return self.assert.gui.CurrentContext().GetView() },
assert: self.assert,
}
}
func (self *ViewAsserterGetter) Main() *Views {
return &Views{
context: "main view",
getView: func() *gocui.View { return self.assert.gui.MainView() },
assert: self.assert,
}
}
func (self *ViewAsserterGetter) Secondary() *Views {
return &Views{
context: "secondary view",
getView: func() *gocui.View { return self.assert.gui.SecondaryView() },
assert: self.assert,
}
}
func (self *ViewAsserterGetter) ByName(viewName string) *Views {
return &Views{
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.assert.gui.View(viewName) },
assert: self.assert,
}
}