2022-08-12 09:19:39 +10:00
package components
2022-08-09 20:27:44 +10:00
import (
"fmt"
"strings"
"time"
2023-01-26 13:25:56 +11:00
"github.com/atotto/clipboard"
2022-08-09 20:27:44 +10:00
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
2022-12-27 21:35:36 +11:00
type TestDriver struct {
2022-12-27 16:27:36 +11:00
gui integrationTypes . GuiDriver
keys config . KeybindingConfig
2022-08-09 20:27:44 +10:00
pushKeyDelay int
2022-12-27 16:27:36 +11:00
* assertionHelper
2022-12-27 21:47:37 +11:00
shell * Shell
2022-08-09 20:27:44 +10:00
}
2022-12-27 21:47:37 +11:00
func NewTestDriver ( gui integrationTypes . GuiDriver , shell * Shell , keys config . KeybindingConfig , pushKeyDelay int ) * TestDriver {
2022-12-27 21:35:36 +11:00
return & TestDriver {
2022-12-27 15:22:31 +11:00
gui : gui ,
keys : keys ,
pushKeyDelay : pushKeyDelay ,
2022-12-27 16:27:36 +11:00
assertionHelper : & assertionHelper { gui : gui } ,
2022-12-27 21:47:37 +11:00
shell : shell ,
2022-08-09 20:27:44 +10:00
}
}
2022-08-09 21:27:12 +10:00
// 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
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) press ( keyStr string ) {
2022-08-09 20:27:44 +10:00
self . Wait ( self . pushKeyDelay )
self . gui . PressKey ( keyStr )
}
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) typeContent ( content string ) {
2022-08-09 20:27:44 +10:00
for _ , char := range content {
2022-12-24 17:48:57 +11:00
self . press ( string ( char ) )
2022-08-09 20:27:44 +10:00
}
}
2022-12-28 10:54:38 +11:00
func ( self * TestDriver ) Actions ( ) * Actions {
return & Actions { t : self }
2022-08-09 20:27:44 +10:00
}
2022-08-09 21:27:12 +10:00
// for when you want to allow lazygit to process something before continuing
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) Wait ( milliseconds int ) {
2022-08-09 20:27:44 +10:00
time . Sleep ( time . Duration ( milliseconds ) * time . Millisecond )
}
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) LogUI ( message string ) {
2022-08-09 20:27:44 +10:00
self . gui . LogUI ( message )
}
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) Log ( message string ) {
2022-08-09 20:27:44 +10:00
self . gui . LogUI ( message )
}
2022-12-27 21:47:37 +11:00
// allows the user to run shell commands during the test to emulate background activity
func ( self * TestDriver ) Shell ( ) * Shell {
return self . shell
}
2022-08-09 21:27:12 +10:00
// 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:
2022-08-12 09:24:39 +10:00
// - 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
2022-08-09 21:27:12 +10:00
//
2022-08-09 20:27:44 +10:00
// 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.
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) navigateToListItem ( matcher * matcher ) {
2022-12-27 15:22:31 +11:00
self . inListContext ( )
2022-08-09 20:27:44 +10:00
currentContext := self . gui . CurrentContext ( ) . ( types . IListContext )
view := currentContext . GetView ( )
2022-09-09 21:11:05 -07:00
var matchIndex int
2022-12-27 16:27:36 +11:00
self . assertWithRetries ( func ( ) ( bool , string ) {
2022-09-09 21:11:05 -07:00
matchIndex = - 1
2022-12-24 17:48:57 +11:00
var matches [ ] string
2022-12-26 17:15:33 +11:00
lines := view . ViewBufferLines ( )
2022-09-09 21:11:05 -07:00
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
2022-12-26 17:15:33 +11:00
for i , line := range lines {
2022-12-24 17:48:57 +11:00
ok , _ := matcher . test ( line )
if ok {
matches = append ( matches , line )
2022-09-09 21:11:05 -07:00
matchIndex = i
}
2022-08-09 20:27:44 +10:00
}
2022-12-24 17:48:57 +11:00
if len ( matches ) > 1 {
2022-12-26 17:15:33 +11:00
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" ) )
2022-12-24 17:48:57 +11:00
} else if len ( matches ) == 0 {
2022-12-26 17:15:33 +11:00
return false , fmt . Sprintf ( "Could not find item matching: %s. Lines:\n%s" , matcher . name ( ) , strings . Join ( lines , "\n" ) )
2022-09-09 21:11:05 -07:00
} else {
return true , ""
}
} )
selectedLineIdx := view . SelectedLineIdx ( )
if selectedLineIdx == matchIndex {
2022-12-27 16:27:36 +11:00
self . Views ( ) . current ( ) . SelectedLine ( matcher )
2022-09-09 21:11:05 -07:00
return
2022-08-09 20:27:44 +10:00
}
2022-09-09 21:11:05 -07:00
if selectedLineIdx < matchIndex {
for i := selectedLineIdx ; i < matchIndex ; i ++ {
2022-12-27 16:27:36 +11:00
self . Views ( ) . current ( ) . SelectNextItem ( )
2022-08-09 20:27:44 +10:00
}
2022-12-27 16:27:36 +11:00
self . Views ( ) . current ( ) . SelectedLine ( matcher )
2022-09-09 21:11:05 -07:00
return
} else {
for i := selectedLineIdx ; i > matchIndex ; i -- {
2022-12-27 16:27:36 +11:00
self . Views ( ) . current ( ) . SelectPreviousItem ( )
2022-08-09 20:27:44 +10:00
}
2022-12-27 16:27:36 +11:00
self . Views ( ) . current ( ) . SelectedLine ( matcher )
2022-09-09 21:11:05 -07:00
return
2022-08-09 20:27:44 +10:00
}
}
2022-12-24 17:48:57 +11:00
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) inListContext ( ) {
2022-12-27 15:22:31 +11:00
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 ( ) )
} )
}
2022-12-27 16:27:36 +11:00
// for making assertions on lazygit views
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) Views ( ) * Views {
return & Views { t : self }
2022-12-27 16:27:36 +11:00
}
2022-12-28 11:00:22 +11:00
// for interacting with popups
func ( self * TestDriver ) ExpectPopup ( ) * Popup {
return & Popup { t : self }
}
2023-01-26 13:25:56 +11:00
func ( self * TestDriver ) ExpectToast ( matcher * matcher ) {
self . Views ( ) . AppStatus ( ) . Content ( matcher )
}
func ( self * TestDriver ) ExpectClipboard ( matcher * matcher ) {
self . assertWithRetries ( func ( ) ( bool , string ) {
text , err := clipboard . ReadAll ( )
if err != nil {
return false , "Error occured when reading from clipboard: " + err . Error ( )
}
ok , _ := matcher . test ( text )
return ok , fmt . Sprintf ( "Expected clipboard to match %s, but got %s" , matcher . name ( ) , text )
} )
}
2022-12-27 22:52:20 +11:00
// for making assertions through git itself
func ( self * TestDriver ) Git ( ) * Git {
return & Git { assertionHelper : self . assertionHelper , shell : self . shell }
2022-12-27 16:27:36 +11:00
}
// for making assertions on the file system
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) FileSystem ( ) * FileSystem {
2022-12-27 16:27:36 +11:00
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.
2022-12-27 21:35:36 +11:00
func ( self * TestDriver ) Fail ( message string ) {
2022-12-27 16:27:36 +11:00
self . assertionHelper . fail ( message )
}