Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ Initialize Ebitest with `ebitest.Run(t, g)` with `t *testing.Test` and `g ebiten
* `WithFace|Color`: To set the default values when the using the assertions with a text value.
* `WithDumpErrorImages`: Which will generate an image when a test fail with the failed assertion on the folder `_ebitest_dump/`

If you need some extra interactions that are not implemented (yet) you can directly use [robotgo](https://github.com/go-vgo/robotgo).
If you need some extra interactions that are not implemented (yet) you can directly use [robotgo](https://github.com/go-vgo/robotgo),
but those may fail as they are not synchronized internally so I would recommend opening an issue and I'll add it.

## Example

Expand All @@ -66,7 +67,7 @@ import (
func TestGameUI(t *testing.T) {
face, _ := loadFont(20)
g := newGameUI()
et := ebitest.Run(t, g,
et := ebitest.Run(g,
ebitest.WithFace(face),
ebitest.WithColor(color.White),
ebitest.WithDumpErrorImages(),
Expand All @@ -76,13 +77,13 @@ func TestGameUI(t *testing.T) {
text1 := "Click Me"
text2 := "Clicked Me"

t1s, _ := et.Should(text1)
et.ShouldNot(text2)
t1s, _ := et.Should(t, text1)
et.ShouldNot(t, text2)

t1s.Click()

et.Should(text1)
et.Should(text2)
et.Should(t, text1)
et.Should(t, text2)
}
```

Expand Down Expand Up @@ -126,11 +127,10 @@ Basically you cannot run more than one test as even calling `Ebitest.Close()` th
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.
I kind of fixed it (100 consecutive test pass) using a custom [PingPong](./ping_pong.go) and [TicTacToe](./tic_tac_toe.go) that basically forces a context switch and synchronizes Input+Game.Update+Game.Draw 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)
* Add more inputs (potentially just port all the [robotgo](https://github.com/go-vgo/robotgo) lib)
* Add more inputs (potentially just port all the [robotgo](https://github.com/go-vgo/robotgo) lib) synchronized
* Others
62 changes: 32 additions & 30 deletions ebitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
Expand All @@ -24,7 +25,6 @@ const (

type Ebitest struct {
game *Game
t *testing.T
PingPong *PingPong

ctxCancelFn context.CancelFunc
Expand Down Expand Up @@ -63,7 +63,7 @@ func WithDumpErrorImages() optionsFn {
}
}

func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest {
func Run(game ebiten.Game, opts ...optionsFn) *Ebitest {
ctx, cfn := context.WithCancel(context.TODO())
pingPong := NewPingPong()
g := newGame(ctx, game, pingPong)
Expand All @@ -76,7 +76,6 @@ func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest {
et := &Ebitest{
game: g,
ctxCancelFn: cfn,
t: t,
PingPong: pingPong,
endGameChan: endGameChan,
}
Expand Down Expand Up @@ -107,18 +106,19 @@ func (e *Ebitest) Close() {

// 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()
func (e *Ebitest) Should(t *testing.T, s interface{}) (*Selector, bool) {
t.Helper()
e.PingPong.Ping()
sc := e.game.GetScreen()

sel, ok := e.findSelector(s)
sel, ok := e.findSelector(sc, s)
if !ok {
msg := "selector not found"
if e.options.dumpErrorImages {
p := dumpErrorImages(e.game.GetScreen(), sel.Image())
p := dumpErrorImages(sc, sel.Image())
msg += "\nimage at: " + p
}
assert.Fail(e.t, msg)
assert.Fail(t, msg)
return nil, false
}

Expand All @@ -127,39 +127,41 @@ func (e *Ebitest) Should(s interface{}) (*Selector, bool) {

// ShouldNot checks if selector(s) is not present in the game
// s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector'
func (e *Ebitest) ShouldNot(s interface{}) bool {
e.t.Helper()
func (e *Ebitest) ShouldNot(t *testing.T, s interface{}) bool {
t.Helper()
e.PingPong.Ping()
sc := e.game.GetScreen()

sel, ok := e.findSelector(s)
sel, ok := e.findSelector(sc, s)
if !ok {
return true
}

msg := "selector found"
if e.options.dumpErrorImages {
p := dumpErrorImages(e.game.GetScreen(), sel.Image())
p := dumpErrorImages(sc, sel.Image())
msg += "\nimage at: " + p
}
assert.Fail(e.t, msg)
assert.Fail(t, msg)
return false
}

// Must checks if selector(s) is present in the game and returns it.
// If it's not present it'll fail the test
// s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector'
func (e *Ebitest) Must(s interface{}) *Selector {
e.t.Helper()
func (e *Ebitest) Must(t *testing.T, s interface{}) *Selector {
t.Helper()
e.PingPong.Ping()
sc := e.game.GetScreen()

sel, ok := e.findSelector(s)
sel, ok := e.findSelector(sc, s)
if !ok {
msg := "selector not found"
if e.options.dumpErrorImages {
p := dumpErrorImages(e.game.GetScreen(), sel.Image())
p := dumpErrorImages(sc, sel.Image())
msg += "\nimage at: " + p
}
assert.Fail(e.t, msg)
require.Fail(t, msg)
return nil
}

Expand All @@ -169,21 +171,22 @@ func (e *Ebitest) Must(s interface{}) *Selector {
// MustNot checks if selector(s) is not present in the game.
// If it's present it'll fail the test
// s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector'
func (e *Ebitest) MustNot(s interface{}) {
e.t.Helper()
func (e *Ebitest) MustNot(t *testing.T, s interface{}) {
t.Helper()
e.PingPong.Ping()
sc := e.game.GetScreen()

sel, ok := e.findSelector(s)
sel, ok := e.findSelector(sc, s)
if !ok {
return
}

msg := "selector found"
if e.options.dumpErrorImages {
p := dumpErrorImages(e.game.GetScreen(), sel.Image())
p := dumpErrorImages(sc, sel.Image())
msg += "\nimage at: " + p
}
assert.Fail(e.t, msg)
require.Fail(t, msg)
return
}

Expand All @@ -204,16 +207,14 @@ func (e *Ebitest) getSelector(s interface{}) *Selector {
}

// findSelector returns a Selector from ss if found
func (e *Ebitest) findSelector(ss interface{}) (*Selector, bool) {
func (e *Ebitest) findSelector(sc image.Image, ss interface{}) (*Selector, bool) {
sel := e.getSelector(ss)

s := e.game.GetScreen()

sx := s.Bounds().Dx()
sy := s.Bounds().Dy()
sx := sc.Bounds().Dx()
sy := sc.Bounds().Dy()
for x := range sx {
for y := range sy {
if hasImageAt(s, sel.Image(), x, y) {
if hasImageAt(sc, sel.Image(), x, y) {
selx := sel.Image().Bounds().Dx()
sely := sel.Image().Bounds().Dy()

Expand Down Expand Up @@ -244,7 +245,8 @@ func dumpErrorImages(s, i image.Image) string {
ip := filepath.Join(baseDumpFoler, u.String()+".png")
writeImage(ip, img)

return ip
wd, _ := os.Getwd()
return filepath.Join(wd, ip)
}

// writeImage writes on the path the image i
Expand Down
10 changes: 5 additions & 5 deletions ebitest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func TestGameButton(t *testing.T) {
face, _ := testdata.LoadFont(20)
g := testdata.NewGame()
et := ebitest.Run(t, g,
et := ebitest.Run(g,
ebitest.WithFace(face),
ebitest.WithColor(color.White),
ebitest.WithDumpErrorImages(),
Expand All @@ -30,11 +30,11 @@ func TestGameButton(t *testing.T) {
text1 := "Click Me"
text2 := "Clicked Me"

t1s, _ := et.Should(text1)
et.ShouldNot(text2)
t1s, _ := et.Should(t, text1)
et.ShouldNot(t, text2)

t1s.Click()

et.ShouldNot(text1)
et.Should(text2)
et.ShouldNot(t, text1)
et.Should(t, text2)
}
11 changes: 10 additions & 1 deletion game.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)

type Game struct {
Expand All @@ -16,13 +17,16 @@ type Game struct {
ctx context.Context

pingPong *PingPong

clickTTT *TicTacToe
}

func newGame(ctx context.Context, g ebiten.Game, pp *PingPong) *Game {
return &Game{
game: g,
ctx: ctx,
pingPong: pp,
clickTTT: NewTicTacToe(),
}
}

Expand Down Expand Up @@ -50,8 +54,11 @@ func (g *Game) Update() error {
case <-g.ctx.Done():
return ebiten.Termination
default:
return g.game.Update()
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
g.clickTTT.Tac()
}
return g.game.Update()
}

// Draw implements Ebiten's Draw method.
Expand All @@ -61,6 +68,8 @@ func (g *Game) Draw(screen *ebiten.Image) {
g.SetScreen(ebitenImageToImage(screen))

g.pingPong.Pong()
g.pingPong.ClickPong(g.clickTTT)
g.clickTTT.Toe()

return
}
35 changes: 35 additions & 0 deletions ping_pong.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package ebitest

import (
"github.com/go-vgo/robotgo"
)

// PingPong used to switch contexts between goroutines
type PingPong struct {
ping chan struct{}
pong chan struct{}

clickPing chan Ball
clickPong chan struct{}
}

type Ball struct {
X, Y int
}

// NewPingPong initialines a new PingPong
func NewPingPong() *PingPong {
return &PingPong{
ping: make(chan struct{}, 1),
pong: make(chan struct{}, 1),

clickPing: make(chan Ball, 1),
clickPong: make(chan struct{}, 1),
}
}

Expand All @@ -28,3 +42,24 @@ func (pp *PingPong) Pong() {
pp.pong <- struct{}{}
}
}

// Ping sends a ping and waits for a Pong
func (pp *PingPong) ClickPing(b Ball) {
pp.clickPing <- b
<-pp.clickPong
}

// Pong will read on ping if any present and then
// will send to pong
func (pp *PingPong) ClickPong(ttt *TicTacToe) {
if len(pp.clickPing) != 0 {
b := <-pp.clickPing
robotgo.Move(b.X, b.Y)
robotgo.Click("left")
ttt.Tic()
go func() {
<-ttt.toe
pp.clickPong <- struct{}{}
}()
}
}
7 changes: 3 additions & 4 deletions selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"image"
"image/color"

"github.com/go-vgo/robotgo"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text/v2"
)
Expand Down Expand Up @@ -39,9 +38,9 @@ func NewFromImage(i image.Image) *Selector {

func (s *Selector) Click() {
cx, cy := s.center()
robotgo.Move(cx, cy)
robotgo.Click("left", true)
s.PingPong.Ping()
//robotgo.Move(cx, cy)
//robotgo.Click("left")
s.PingPong.ClickPing(Ball{X: cx, Y: cy})
}

func (s *Selector) center() (int, int) {
Expand Down
Loading
Loading