2022-12-27 07:27:36 +02:00
package components
import (
"fmt"
2022-12-27 13:52:20 +02:00
"strings"
2022-12-27 07:27:36 +02:00
"github.com/jesseduffield/gocui"
2023-02-12 00:53:57 +02:00
"github.com/samber/lo"
2022-12-27 07:27:36 +02:00
)
2022-12-28 02:27:48 +02:00
type ViewDriver struct {
2022-12-27 07:27:36 +02:00
// context is prepended to any error messages e.g. 'context: "current view"'
2023-02-24 12:42:27 +02:00
context string
getView func ( ) * gocui . View
t * TestDriver
getSelectedLinesFn func ( ) ( [ ] string , error )
2022-12-27 07:27:36 +02:00
}
// asserts that the view has the expected title
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) Title ( expected * matcher ) * ViewDriver {
2022-12-27 12:35:36 +02:00
self . t . assertWithRetries ( func ( ) ( bool , string ) {
2022-12-27 07:27:36 +02:00
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.
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) TopLines ( matchers ... * matcher ) * ViewDriver {
2022-12-27 13:52:20 +02:00
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()" )
}
2022-12-27 12:35:36 +02:00
self . t . assertWithRetries ( func ( ) ( bool , string ) {
2022-12-27 07:27:36 +02:00
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.
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) Lines ( matchers ... * matcher ) * ViewDriver {
2022-12-27 13:52:20 +02:00
self . LineCount ( len ( matchers ) )
2022-12-27 07:27:36 +02:00
return self . assertLines ( matchers ... )
}
2023-02-24 12:42:27 +02:00
func ( self * ViewDriver ) getSelectedLines ( ) ( [ ] string , error ) {
if self . getSelectedLinesFn == nil {
view := self . t . gui . View ( self . getView ( ) . Name ( ) )
return [ ] string { view . SelectedLine ( ) } , nil
}
return self . getSelectedLinesFn ( )
}
func ( self * ViewDriver ) SelectedLines ( matchers ... * matcher ) * ViewDriver {
self . t . assertWithRetries ( func ( ) ( bool , string ) {
selectedLines , err := self . getSelectedLines ( )
if err != nil {
return false , err . Error ( )
}
selectedContent := strings . Join ( selectedLines , "\n" )
expectedContent := expectedContentFromMatchers ( matchers )
if len ( selectedLines ) != len ( matchers ) {
return false , fmt . Sprintf ( "Expected the following to be selected:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----" , expectedContent , selectedContent )
}
for i , line := range selectedLines {
ok , message := matchers [ i ] . test ( line )
if ! ok {
return false , fmt . Sprintf ( "Error: %s. Expected the following to be selected:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----" , message , expectedContent , selectedContent )
}
}
return true , ""
} )
return self
}
func ( self * ViewDriver ) ContainsLines ( matchers ... * matcher ) * ViewDriver {
self . t . assertWithRetries ( func ( ) ( bool , string ) {
content := self . getView ( ) . Buffer ( )
lines := strings . Split ( content , "\n" )
for i := 0 ; i < len ( lines ) - len ( matchers ) + 1 ; i ++ {
matches := true
for j , matcher := range matchers {
ok , _ := matcher . test ( lines [ i + j ] )
if ! ok {
matches = false
break
}
}
if matches {
return true , ""
}
}
expectedContent := expectedContentFromMatchers ( matchers )
return false , fmt . Sprintf (
"Expected the following to be contained in the staging panel:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----" ,
expectedContent ,
content ,
)
} )
return self
}
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) assertLines ( matchers ... * matcher ) * ViewDriver {
2022-12-27 07:27:36 +02:00
view := self . getView ( )
for i , matcher := range matchers {
checkIsSelected , matcher := matcher . checkIsSelected ( )
2022-12-27 12:35:36 +02:00
self . t . matchString ( matcher , fmt . Sprintf ( "Unexpected content in view '%s'." , view . Name ( ) ) ,
2022-12-27 07:27:36 +02:00
func ( ) string {
return view . BufferLines ( ) [ i ]
} ,
)
if checkIsSelected {
2022-12-27 12:35:36 +02:00
self . t . assertWithRetries ( func ( ) ( bool , string ) {
2022-12-27 07:27:36 +02:00
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.
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) Content ( matcher * matcher ) * ViewDriver {
2022-12-27 12:35:36 +02:00
self . t . matchString ( matcher , fmt . Sprintf ( "%s: Unexpected content." , self . context ) ,
2022-12-27 07:27:36 +02:00
func ( ) string {
return self . getView ( ) . Buffer ( )
} ,
)
return self
}
// asserts on the selected line of the view
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) SelectedLine ( matcher * matcher ) * ViewDriver {
2023-02-24 12:42:27 +02:00
self . t . assertWithRetries ( func ( ) ( bool , string ) {
selectedLines , err := self . getSelectedLines ( )
if err != nil {
return false , err . Error ( )
}
if len ( selectedLines ) == 0 {
return false , "No line selected. Expected exactly one line to be selected"
} else if len ( selectedLines ) > 1 {
return false , fmt . Sprintf (
"Multiple lines selected. Expected only a single line to be selected. Selected lines:\n---\n%s\n---\n\nExpected line: %s" ,
strings . Join ( selectedLines , "\n" ) ,
matcher . name ( ) ,
)
}
value := selectedLines [ 0 ]
return matcher . context ( fmt . Sprintf ( "%s: Unexpected selected line." , self . context ) ) . test ( value )
} )
2022-12-27 12:35:36 +02:00
self . t . matchString ( matcher , fmt . Sprintf ( "%s: Unexpected selected line." , self . context ) ,
2022-12-27 07:27:36 +02:00
func ( ) string {
2023-02-24 12:42:27 +02:00
selectedLines , err := self . getSelectedLines ( )
if err != nil {
self . t . gui . Fail ( err . Error ( ) )
return "<failed to obtain selected line>"
}
return selectedLines [ 0 ]
2022-12-27 07:27:36 +02:00
} ,
)
return self
}
// asserts on the index of the selected line. 0 is the first index, representing the line at the top of the view.
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) SelectedLineIdx ( expected int ) * ViewDriver {
2022-12-27 12:35:36 +02:00
self . t . assertWithRetries ( func ( ) ( bool , string ) {
2022-12-27 07:27:36 +02:00
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
}
2023-02-12 00:53:57 +02:00
// focus the view (assumes the view is a side-view)
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) Focus ( ) * ViewDriver {
2022-12-27 07:27:36 +02:00
viewName := self . getView ( ) . Name ( )
2023-02-12 00:53:57 +02:00
type window struct {
name string
viewNames [ ] string
2022-12-27 07:27:36 +02:00
}
2023-02-12 00:53:57 +02:00
windows := [ ] window {
{ name : "status" , viewNames : [ ] string { "status" } } ,
{ name : "files" , viewNames : [ ] string { "files" , "submodules" } } ,
{ name : "branches" , viewNames : [ ] string { "localBranches" , "remotes" , "tags" } } ,
{ name : "commits" , viewNames : [ ] string { "commits" , "reflogCommits" } } ,
{ name : "stash" , viewNames : [ ] string { "stash" } } ,
2022-12-27 07:27:36 +02:00
}
2023-02-12 00:53:57 +02:00
for windowIndex , window := range windows {
if lo . Contains ( window . viewNames , viewName ) {
tabIndex := lo . IndexOf ( window . viewNames , viewName )
// jump to the desired window
self . t . press ( self . t . keys . Universal . JumpToBlock [ windowIndex ] )
2022-12-27 07:27:36 +02:00
2023-02-12 00:53:57 +02:00
// assert we're in the window before continuing
self . t . assertWithRetries ( func ( ) ( bool , string ) {
currentWindowName := self . t . gui . CurrentContext ( ) . GetWindowName ( )
// by convention the window is named after the first view in the window
return currentWindowName == window . name , fmt . Sprintf ( "Expected to be in window '%s', but was in '%s'" , window . name , currentWindowName )
} )
// switch to the desired tab
currentViewName := self . t . gui . CurrentContext ( ) . GetViewName ( )
currentViewTabIndex := lo . IndexOf ( window . viewNames , currentViewName )
if tabIndex > currentViewTabIndex {
for i := 0 ; i < tabIndex - currentViewTabIndex ; i ++ {
self . t . press ( self . t . keys . Universal . NextTab )
}
} else if tabIndex < currentViewTabIndex {
for i := 0 ; i < currentViewTabIndex - tabIndex ; i ++ {
self . t . press ( self . t . keys . Universal . PrevTab )
}
}
// assert that we're now in the expected view
self . IsFocused ( )
return self
}
}
self . t . fail ( fmt . Sprintf ( "Cannot focus view %s: Focus() method not implemented" , viewName ) )
2022-12-27 07:27:36 +02:00
return self
}
// asserts that the view is focused
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) IsFocused ( ) * ViewDriver {
2022-12-27 12:35:36 +02:00
self . t . assertWithRetries ( func ( ) ( bool , string ) {
2022-12-27 07:27:36 +02:00
expected := self . getView ( ) . Name ( )
2022-12-27 12:35:36 +02:00
actual := self . t . gui . CurrentContext ( ) . GetView ( ) . Name ( )
2022-12-27 07:27:36 +02:00
return actual == expected , fmt . Sprintf ( "%s: Unexpected view focused. Expected %s, got %s" , self . context , expected , actual )
} )
return self
}
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) Press ( keyStr string ) * ViewDriver {
2022-12-27 07:27:36 +02:00
self . IsFocused ( )
2022-12-27 12:35:36 +02:00
self . t . press ( keyStr )
2022-12-27 07:27:36 +02:00
return self
}
// i.e. pressing down arrow
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) SelectNextItem ( ) * ViewDriver {
2022-12-27 12:35:36 +02:00
return self . Press ( self . t . keys . Universal . NextItem )
2022-12-27 07:27:36 +02:00
}
// i.e. pressing up arrow
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) SelectPreviousItem ( ) * ViewDriver {
2022-12-27 12:35:36 +02:00
return self . Press ( self . t . keys . Universal . PrevItem )
2022-12-27 07:27:36 +02:00
}
// i.e. pressing space
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) PressPrimaryAction ( ) * ViewDriver {
2022-12-27 12:35:36 +02:00
return self . Press ( self . t . keys . Universal . Select )
2022-12-27 07:27:36 +02:00
}
// i.e. pressing space
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) PressEnter ( ) * ViewDriver {
2022-12-27 12:35:36 +02:00
return self . Press ( self . t . keys . Universal . Confirm )
2022-12-27 07:27:36 +02:00
}
// i.e. pressing escape
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) PressEscape ( ) * ViewDriver {
2022-12-27 12:35:36 +02:00
return self . Press ( self . t . keys . Universal . Return )
2022-12-27 07:27:36 +02:00
}
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) NavigateToListItem ( matcher * matcher ) * ViewDriver {
2022-12-27 07:27:36 +02:00
self . IsFocused ( )
2022-12-27 12:35:36 +02:00
self . t . navigateToListItem ( matcher )
2022-12-27 07:27:36 +02:00
return self
}
2022-12-27 12:25:11 +02:00
2022-12-27 13:52:20 +02:00
// returns true if the view is a list view and it contains no items
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) IsEmpty ( ) * ViewDriver {
2022-12-27 13:52:20 +02:00
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
}
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) LineCount ( expectedCount int ) * ViewDriver {
2022-12-27 13:52:20 +02:00
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 ( ) )
2022-12-28 01:29:32 +02:00
return actual != "" , "unexpected number of lines in view. Expected 1, got 0"
2022-12-27 13:52:20 +02:00
}
return len ( lines ) == expectedCount , fmt . Sprintf ( "unexpected number of lines in view. Expected %d, got %d" , expectedCount , len ( lines ) )
} )
return self
}
2022-12-27 12:25:11 +02:00
// for when you want to make some assertion unrelated to the current view
// without breaking the method chain
2022-12-28 02:27:48 +02:00
func ( self * ViewDriver ) Tap ( f func ( ) ) * ViewDriver {
2022-12-27 12:25:11 +02:00
f ( )
return self
}
2023-02-24 12:42:27 +02:00
func expectedContentFromMatchers ( matchers [ ] * matcher ) string {
return strings . Join ( lo . Map ( matchers , func ( matcher * matcher , _ int ) string {
return matcher . name ( )
} ) , "\n" )
}