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 @@
+[](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.