diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..ed7dea5 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,33 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: "Update the pkgs" + run: sudo apt-get update + # We install xorg-dev because of https://github.com/go-gl/glfw/issues/129#issuecomment-75928365 + # We install xvfb because of the error "The DISPLAY environment variable is missing" so we need + # to have a fake DISPLAY and xvfb does exactly that. + # Found it in https://stackoverflow.com/questions/834723/a-dev-null-equivilent-for-display-when-the-display-is-just-noise + - name: "Install xorg-dev and xvfb" + run: sudo apt-get install xvfb xorg-dev + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.25' + + - name: Test + run: make test ARGS="-v" diff --git a/README.md b/README.md index 8e6caba..ede8def 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Go Reference](https://pkg.go.dev/badge/github.com/xescugc/ebitest.svg)](https://pkg.go.dev/github.com/xescugc/ebitest) + # Ebitest Ebitest is a lib to test Ebiten UI through inputs and asserting on what should be on the screen. @@ -106,6 +108,27 @@ And if you open the `_ebitest_dump/019b1537-1c60-7041-ad54-0297ea4b0eef.png` (on

+## Run it on a CI + +If the CI has low resources (like GitHub Actions) it'll most likely fail (check `Known issues#2`) but you +can check what I install for it to run on the [`go.yml`](.github/workflows/go.yml) + +## Known issues and Limitations + +1/ You cannot have more than 1 test case + +Basically you cannot run more than one test as even calling `Ebitest.Close()` there are some resources missing and you may get an error like + +> panic: ebiten: NewImage cannot be called after RunGame finishes [recovered, repanicked] + +2/ Some false positive/negative + +Due to the nature of this test (the game is running on a goroutine) there may be the case in which an input is not registered by the game +so an expectation may randomly fail. + +I kind of fixed it using a custom [PingPong](./ping_pong.go) that basically forces a context switch but it still fails in low resource like +GitHub [Actions](https://github.com/xescugc/ebitest/actions) for example. + ## Plans * Add more helpers for assertions (like animations) diff --git a/ebitest.go b/ebitest.go index d7b2781..bb392b1 100644 --- a/ebitest.go +++ b/ebitest.go @@ -25,9 +25,10 @@ const ( type Ebitest struct { game *Game t *testing.T - pingPong *PingPong + PingPong *PingPong ctxCancelFn context.CancelFunc + endGameChan chan struct{} options options } @@ -66,13 +67,18 @@ func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest { ctx, cfn := context.WithCancel(context.TODO()) pingPong := NewPingPong() g := newGame(ctx, game, pingPong) - go ebiten.RunGame(g) + endGameChan := make(chan struct{}) + go func() { + ebiten.RunGame(g) + endGameChan <- struct{}{} + }() et := &Ebitest{ game: g, ctxCancelFn: cfn, t: t, - pingPong: pingPong, + PingPong: pingPong, + endGameChan: endGameChan, } op := options{} @@ -82,7 +88,7 @@ func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest { } et.options = op - et.pingPong.Ping() + et.PingPong.Ping() if et.options.dumpErrorImages { os.RemoveAll(baseDumpFoler) @@ -95,13 +101,15 @@ func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest { // Close stops the underlying game func (e *Ebitest) Close() { e.ctxCancelFn() + <-e.endGameChan + close(e.endGameChan) } // Should checks if selector(s) is present in the game and returns it // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' func (e *Ebitest) Should(s interface{}) (*Selector, bool) { e.t.Helper() - e.pingPong.Ping() + e.PingPong.Ping() sel, ok := e.findSelector(s) if !ok { @@ -121,7 +129,7 @@ func (e *Ebitest) Should(s interface{}) (*Selector, bool) { // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' func (e *Ebitest) ShouldNot(s interface{}) bool { e.t.Helper() - e.pingPong.Ping() + e.PingPong.Ping() sel, ok := e.findSelector(s) if !ok { @@ -142,7 +150,7 @@ func (e *Ebitest) ShouldNot(s interface{}) bool { // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' func (e *Ebitest) Must(s interface{}) *Selector { e.t.Helper() - e.pingPong.Ping() + e.PingPong.Ping() sel, ok := e.findSelector(s) if !ok { @@ -163,7 +171,7 @@ func (e *Ebitest) Must(s interface{}) *Selector { // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' func (e *Ebitest) MustNot(s interface{}) { e.t.Helper() - e.pingPong.Ping() + e.PingPong.Ping() sel, ok := e.findSelector(s) if !ok { @@ -210,6 +218,7 @@ func (e *Ebitest) findSelector(ss interface{}) (*Selector, bool) { sely := sel.Image().Bounds().Dy() sel.rect = image.Rect(x, y, x+selx, y+sely) + sel.PingPong = e.PingPong return sel, true } } diff --git a/ebitest_test.go b/ebitest_test.go index ce65ff7..fb5c141 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -4,12 +4,15 @@ import ( "image/color" "testing" + "github.com/go-vgo/robotgo" + "github.com/stretchr/testify/assert" "github.com/xescugc/ebitest" + "github.com/xescugc/ebitest/testdata" ) -func TestGameUI(t *testing.T) { - face, _ := loadFont(20) - g := newGameUI() +func TestGameButton(t *testing.T) { + face, _ := testdata.LoadFont(20) + g := testdata.NewGame() et := ebitest.Run(t, g, ebitest.WithFace(face), ebitest.WithColor(color.White), @@ -17,6 +20,13 @@ func TestGameUI(t *testing.T) { ) defer et.Close() + robotgo.Move(0, 0) + robotgo.Click("left", true) + + assert.True(t, g.Clicked) + + et.PingPong.Ping() + text1 := "Click Me" text2 := "Clicked Me" @@ -25,6 +35,6 @@ func TestGameUI(t *testing.T) { t1s.Click() - et.Should(text1) + et.ShouldNot(text1) et.Should(text2) } diff --git a/go.mod b/go.mod index 100d082..c2f0909 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/xescugc/ebitest -go 1.25.1 +go 1.25 require ( github.com/ebitenui/ebitenui v0.7.2 diff --git a/selector.go b/selector.go index 266be29..750a555 100644 --- a/selector.go +++ b/selector.go @@ -12,6 +12,8 @@ import ( type Selector struct { img image.Image rect image.Rectangle + + PingPong *PingPong } func NewFromText(txt string, f text.Face, c color.Color) *Selector { @@ -39,6 +41,7 @@ func (s *Selector) Click() { cx, cy := s.center() robotgo.Move(cx, cy) robotgo.Click("left", true) + s.PingPong.Ping() } func (s *Selector) center() (int, int) { diff --git a/gameui_test.go b/testdata/game.go similarity index 87% rename from gameui_test.go rename to testdata/game.go index 57252f5..7f440ac 100644 --- a/gameui_test.go +++ b/testdata/game.go @@ -1,4 +1,4 @@ -package ebitest_test +package testdata import ( "bytes" @@ -10,22 +10,25 @@ import ( "github.com/ebitenui/ebitenui/image" "github.com/ebitenui/ebitenui/widget" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text/v2" "golang.org/x/image/font/gofont/goregular" ) // Game object used by ebiten. -type gameui struct { +type Game struct { ui *ebitenui.UI btn *widget.Button + + Clicked bool } -func newGameUI() *gameui { +func NewGame() *Game { // load images for button states: idle, hover, and pressed. buttonImage, _ := loadButtonImage() // load button text font. - face, _ := loadFont(20) + face, _ := LoadFont(20) // construct a new container that serves as the root of the UI hierarchy. rootContainer := widget.NewContainer( @@ -78,7 +81,7 @@ func newGameUI() *gameui { Container: rootContainer, } - game := gameui{ + game := Game{ ui: &ui, btn: button, } @@ -87,12 +90,16 @@ func newGameUI() *gameui { } // Layout implements Game. -func (g *gameui) Layout(outsideWidth int, outsideHeight int) (int, int) { +func (g *Game) Layout(outsideWidth int, outsideHeight int) (int, int) { return outsideWidth, outsideHeight } // Update implements Game. -func (g *gameui) Update() error { +func (g *Game) Update() error { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + g.Clicked = true + } + // update the UI g.ui.Update() @@ -100,13 +107,12 @@ func (g *gameui) Update() error { } // Draw implements Ebiten's Draw method. -func (g *gameui) Draw(screen *ebiten.Image) { +func (g *Game) Draw(screen *ebiten.Image) { // draw the UI onto the screen g.ui.Draw(screen) } func loadButtonImage() (*widget.ButtonImage, error) { - idle := image.NewBorderedNineSliceColor(color.NRGBA{R: 170, G: 170, B: 180, A: 255}, color.NRGBA{90, 90, 90, 255}, 3) hover := image.NewBorderedNineSliceColor(color.NRGBA{R: 130, G: 130, B: 150, A: 255}, color.NRGBA{70, 70, 70, 255}, 3) @@ -120,7 +126,7 @@ func loadButtonImage() (*widget.ButtonImage, error) { }, nil } -func loadFont(size float64) (text.Face, error) { +func LoadFont(size float64) (text.Face, error) { s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF)) if err != nil { log.Fatal(err)