From 2f129c4d1dd77f326c95f0094c01deaf615b5447 Mon Sep 17 00:00:00 2001 From: Francesc Gil Date: Sat, 13 Dec 2025 02:19:24 +0100 Subject: [PATCH 01/15] Modify GitHub Actions workflow for Go project Updated GitHub Actions workflow to install necessary packages and changed test command. --- .github/workflows/go.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..804c19b --- /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.20' + + - name: Test + run: make test ARGS="-v" From 72104a93a9e152ad694723679a40fd92e076a33e Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:21:33 +0100 Subject: [PATCH 02/15] mod: Fix mod error --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e55f38a820364f55323b30b17a1db2e3db6127b0 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:23:21 +0100 Subject: [PATCH 03/15] .github/workflows/go.yml: Upgraded go version --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 804c19b..ed7dea5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.25' - name: Test run: make test ARGS="-v" From da28728e13f1d3715c86fefba18ee176a331d574 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:25:07 +0100 Subject: [PATCH 04/15] ebitest: Fixed a broken test --- ebitest_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebitest_test.go b/ebitest_test.go index ce65ff7..72d0e96 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -25,6 +25,6 @@ func TestGameUI(t *testing.T) { t1s.Click() - et.Should(text1) + et.ShouldNot(text1) et.Should(text2) } From b60e76720f626db65c243201c6532c7183117c51 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:27:28 +0100 Subject: [PATCH 05/15] asdf --- ebitest_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ebitest_test.go b/ebitest_test.go index 72d0e96..7676484 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -3,6 +3,7 @@ package ebitest_test import ( "image/color" "testing" + "time" "github.com/xescugc/ebitest" ) @@ -24,6 +25,7 @@ func TestGameUI(t *testing.T) { et.ShouldNot(text2) t1s.Click() + t.Sleep(time.Second) et.ShouldNot(text1) et.Should(text2) From 860972b27ba7d99a8c02f277100140630dcad7d5 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:40:13 +0100 Subject: [PATCH 06/15] added pingPong to selector --- ebitest.go | 1 + ebitest_test.go | 2 -- selector.go | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ebitest.go b/ebitest.go index d7b2781..5192fbd 100644 --- a/ebitest.go +++ b/ebitest.go @@ -210,6 +210,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 7676484..72d0e96 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -3,7 +3,6 @@ package ebitest_test import ( "image/color" "testing" - "time" "github.com/xescugc/ebitest" ) @@ -25,7 +24,6 @@ func TestGameUI(t *testing.T) { et.ShouldNot(text2) t1s.Click() - t.Sleep(time.Second) et.ShouldNot(text1) et.Should(text2) diff --git a/selector.go b/selector.go index 266be29..94617c8 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) { From 06aa4e17c50984f1149c98d469a657c1942d1ffd Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:42:31 +0100 Subject: [PATCH 07/15] asdf --- ebitest_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ebitest_test.go b/ebitest_test.go index 72d0e96..c733263 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -3,6 +3,7 @@ package ebitest_test import ( "image/color" "testing" + "time" "github.com/xescugc/ebitest" ) @@ -24,6 +25,7 @@ func TestGameUI(t *testing.T) { et.ShouldNot(text2) t1s.Click() + time.Sleep(time.Second) et.ShouldNot(text1) et.Should(text2) From a929a022b95ec861b96b45ed7af254d27416f832 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:44:18 +0100 Subject: [PATCH 08/15] asdf --- selector.go | 1 + 1 file changed, 1 insertion(+) diff --git a/selector.go b/selector.go index 94617c8..0bbcd4a 100644 --- a/selector.go +++ b/selector.go @@ -38,6 +38,7 @@ func NewFromImage(i image.Image) *Selector { } func (s *Selector) Click() { + s.pingPong.Ping() cx, cy := s.center() robotgo.Move(cx, cy) robotgo.Click("left", true) From 3c86fd170d410d621362805bd5ffd30e1cfaa4e8 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 02:46:14 +0100 Subject: [PATCH 09/15] asd --- ebitest_test.go | 2 -- selector.go | 1 - 2 files changed, 3 deletions(-) diff --git a/ebitest_test.go b/ebitest_test.go index c733263..72d0e96 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -3,7 +3,6 @@ package ebitest_test import ( "image/color" "testing" - "time" "github.com/xescugc/ebitest" ) @@ -25,7 +24,6 @@ func TestGameUI(t *testing.T) { et.ShouldNot(text2) t1s.Click() - time.Sleep(time.Second) et.ShouldNot(text1) et.Should(text2) diff --git a/selector.go b/selector.go index 0bbcd4a..94617c8 100644 --- a/selector.go +++ b/selector.go @@ -38,7 +38,6 @@ func NewFromImage(i image.Image) *Selector { } func (s *Selector) Click() { - s.pingPong.Ping() cx, cy := s.center() robotgo.Move(cx, cy) robotgo.Click("left", true) From 65bb8e3239806d41cba4b0276a59021e5359f508 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 12:44:35 +0100 Subject: [PATCH 10/15] more --- ebitest.go | 26 +++++++++++++++++--------- ebitest_test.go | 15 ++++++++++++--- selector.go | 4 ++-- gameui_test.go => testdata/game.go | 26 ++++++++++++++++---------- 4 files changed, 47 insertions(+), 24 deletions(-) rename gameui_test.go => testdata/game.go (87%) diff --git a/ebitest.go b/ebitest.go index 5192fbd..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,7 +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 + sel.PingPong = e.PingPong return sel, true } } diff --git a/ebitest_test.go b/ebitest_test.go index 72d0e96..679fec4 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -3,13 +3,17 @@ package ebitest_test import ( "image/color" "testing" + "time" + "github.com/go-vgo/robotgo" "github.com/xescugc/ebitest" + "github.com/xescugc/ebitest/testdata" ) -func TestGameUI(t *testing.T) { - face, _ := loadFont(20) - g := newGameUI() +func TestGameButton(t *testing.T) { + time.Sleep(time.Second * 10) + face, _ := testdata.LoadFont(20) + g := testdata.NewGame() et := ebitest.Run(t, g, ebitest.WithFace(face), ebitest.WithColor(color.White), @@ -17,6 +21,11 @@ func TestGameUI(t *testing.T) { ) defer et.Close() + robotgo.Move(0, 0) + robotgo.Click("left", true) + + et.PingPong.Ping() + text1 := "Click Me" text2 := "Clicked Me" diff --git a/selector.go b/selector.go index 94617c8..750a555 100644 --- a/selector.go +++ b/selector.go @@ -13,7 +13,7 @@ type Selector struct { img image.Image rect image.Rectangle - pingPong *PingPong + PingPong *PingPong } func NewFromText(txt string, f text.Face, c color.Color) *Selector { @@ -41,7 +41,7 @@ func (s *Selector) Click() { cx, cy := s.center() robotgo.Move(cx, cy) robotgo.Click("left", true) - s.pingPong.Ping() + 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) From 36731f7049830c8493ddcf966b195e4f86a3758b Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 12:44:57 +0100 Subject: [PATCH 11/15] asd --- ebitest_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ebitest_test.go b/ebitest_test.go index 679fec4..f58cb0f 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -3,7 +3,6 @@ package ebitest_test import ( "image/color" "testing" - "time" "github.com/go-vgo/robotgo" "github.com/xescugc/ebitest" @@ -11,7 +10,6 @@ import ( ) func TestGameButton(t *testing.T) { - time.Sleep(time.Second * 10) face, _ := testdata.LoadFont(20) g := testdata.NewGame() et := ebitest.Run(t, g, From fd6c8a49a34853027b89237b2f5946b663096676 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 12:46:58 +0100 Subject: [PATCH 12/15] asd --- ebitest_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ebitest_test.go b/ebitest_test.go index f58cb0f..4ef4812 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-vgo/robotgo" "github.com/xescugc/ebitest" "github.com/xescugc/ebitest/testdata" + "github.com/zeebo/assert" ) func TestGameButton(t *testing.T) { @@ -22,6 +23,8 @@ func TestGameButton(t *testing.T) { robotgo.Move(0, 0) robotgo.Click("left", true) + assert.True(t, g.Clicked) + et.PingPong.Ping() text1 := "Click Me" From 39520d887802e0fbb6c19802d1d24c50b2a08f98 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 12:48:26 +0100 Subject: [PATCH 13/15] asd --- ebitest_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebitest_test.go b/ebitest_test.go index 4ef4812..fb5c141 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -5,9 +5,9 @@ import ( "testing" "github.com/go-vgo/robotgo" + "github.com/stretchr/testify/assert" "github.com/xescugc/ebitest" "github.com/xescugc/ebitest/testdata" - "github.com/zeebo/assert" ) func TestGameButton(t *testing.T) { From 5c46a0ac1cc8b4f0861e4870611b0413b40c9626 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 13:02:13 +0100 Subject: [PATCH 14/15] asdf --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 8e6caba..d244a84 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,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) From 37510905b01a04e26a802cc2588e9ee7729231b3 Mon Sep 17 00:00:00 2001 From: xescugc Date: Sat, 13 Dec 2025 13:03:21 +0100 Subject: [PATCH 15/15] sadf --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d244a84..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.