diff --git a/cmd/gogio/e2e_test.go b/cmd/gogio/e2e_test.go index bc28e56b..bda20078 100644 --- a/cmd/gogio/e2e_test.go +++ b/cmd/gogio/e2e_test.go @@ -5,6 +5,7 @@ package main_test import ( "flag" "image" + "image/color" "testing" ) @@ -29,64 +30,74 @@ type TestDriver interface { // Screenshot takes a screenshot of the Gio app on the platform. Screenshot() image.Image + + // Click performs a pointer click at the specified coordinates, + // including both press and release. + Click(x, y int) } func runEndToEndTest(t *testing.T, driver TestDriver) { width, height := 800, 600 cleanups := driver.Start(t, "testdata/red.go", width, height) - // We expect to receive a 800x600px screenshot. - img := driver.Screenshot() - size := img.Bounds().Size() - if size.X != width || size.Y != height { - t.Fatalf("expected dimensions to be %d*%d, got %d*%d", - width, height, size.X, size.Y) - } - // The colors are split in four rectangular sections. Check the corners // of each of the sections. We check the corners left to right, top to // bottom, like when reading left-to-right text. - - // The top left should be 0xdeadbe. - { - minX, minY := 5, 5 - maxX, maxY := (width/2)-5, (height/2)-5 - wantColor(t, img, minX, minY, 0xdede, 0xadad, 0xbebe) - wantColor(t, img, maxX, minY, 0xdede, 0xadad, 0xbebe) - wantColor(t, img, minX, maxY, 0xdede, 0xadad, 0xbebe) - wantColor(t, img, maxX, maxY, 0xdede, 0xadad, 0xbebe) + wantColors := func(topLeft, topRight, botLeft, botRight color.RGBA) { + img := driver.Screenshot() + size := img.Bounds().Size() + // We expect to receive a width*height screenshot. + if size.X != width || size.Y != height { + t.Fatalf("expected dimensions to be %d*%d, got %d*%d", + width, height, size.X, size.Y) + } + { + minX, minY := 5, 5 + maxX, maxY := (width/2)-5, (height/2)-5 + wantColor(t, img, minX, minY, topLeft) + wantColor(t, img, maxX, minY, topLeft) + wantColor(t, img, minX, maxY, topLeft) + wantColor(t, img, maxX, maxY, topLeft) + } + { + minX, minY := (width/2)+5, 5 + maxX, maxY := width-5, (height/2)-5 + wantColor(t, img, minX, minY, topRight) + wantColor(t, img, maxX, minY, topRight) + wantColor(t, img, minX, maxY, topRight) + wantColor(t, img, maxX, maxY, topRight) + } + { + minX, minY := 5, (height/2)+5 + maxX, maxY := (width/2)-5, height-5 + wantColor(t, img, minX, minY, botLeft) + wantColor(t, img, maxX, minY, botLeft) + wantColor(t, img, minX, maxY, botLeft) + wantColor(t, img, maxX, maxY, botLeft) + } + { + minX, minY := (width/2)+5, (height/2)+5 + maxX, maxY := width-5, height-5 + wantColor(t, img, minX, minY, botRight) + wantColor(t, img, maxX, minY, botRight) + wantColor(t, img, minX, maxY, botRight) + wantColor(t, img, maxX, maxY, botRight) + } } - // The top right should be 0xffffff. - { - minX, minY := (width/2)+5, 5 - maxX, maxY := width-5, (height/2)-5 - wantColor(t, img, minX, minY, 0xffff, 0xffff, 0xffff) - wantColor(t, img, maxX, minY, 0xffff, 0xffff, 0xffff) - wantColor(t, img, minX, maxY, 0xffff, 0xffff, 0xffff) - wantColor(t, img, maxX, maxY, 0xffff, 0xffff, 0xffff) - } + beef := color.RGBA{R: 0xde, G: 0xad, B: 0xbe} + white := color.RGBA{R: 0xff, G: 0xff, B: 0xff} + black := color.RGBA{R: 0x00, G: 0x00, B: 0x00} + gray := color.RGBA{R: 0xbb, G: 0xbb, B: 0xbb} + red := color.RGBA{R: 0xff, G: 0x00, B: 0x00} - // The bottom left should be 0x000000. - { - minX, minY := 5, (height/2)+5 - maxX, maxY := (width/2)-5, height-5 - wantColor(t, img, minX, minY, 0x0000, 0x0000, 0x0000) - wantColor(t, img, maxX, minY, 0x0000, 0x0000, 0x0000) - wantColor(t, img, minX, maxY, 0x0000, 0x0000, 0x0000) - wantColor(t, img, maxX, maxY, 0x0000, 0x0000, 0x0000) - } + // These are the four colors at the beginning. + wantColors(beef, white, black, gray) - // The bottom right is black (0x000000) with 0x80 alpha, so we should - // see gray (0xbbbbbb). - { - minX, minY := (width/2)+5, (height/2)+5 - maxX, maxY := width-5, height-5 - wantColor(t, img, minX, minY, 0xbbbb, 0xbbbb, 0xbbbb) - wantColor(t, img, maxX, minY, 0xbbbb, 0xbbbb, 0xbbbb) - wantColor(t, img, minX, maxY, 0xbbbb, 0xbbbb, 0xbbbb) - wantColor(t, img, maxX, maxY, 0xbbbb, 0xbbbb, 0xbbbb) - } + // Click the first and last sections to turn them red. + driver.Click(1*(width/4), 1*(height/4)) + driver.Click(3*(width/4), 3*(height/4)) + wantColors(red, white, black, red) // Run the cleanup funcs from last to first, as if they were defers. for i := len(cleanups) - 1; i >= 0; i-- { @@ -94,9 +105,10 @@ func runEndToEndTest(t *testing.T, driver TestDriver) { } } -func wantColor(t *testing.T, img image.Image, x, y int, r, g, b uint32) { - color := img.At(x, y) - r_, g_, b_, _ := color.RGBA() +func wantColor(t *testing.T, img image.Image, x, y int, want color.Color) { + r, g, b, _ := want.RGBA() + got := img.At(x, y) + r_, g_, b_, _ := got.RGBA() if r_ != r || g_ != g || b_ != b { t.Errorf("got 0x%04x%04x%04x at (%d,%d), want 0x%04x%04x%04x", r_, g_, b_, x, y, r, g, b) diff --git a/cmd/gogio/js_test.go b/cmd/gogio/js_test.go index fb00cea3..627a24af 100644 --- a/cmd/gogio/js_test.go +++ b/cmd/gogio/js_test.go @@ -15,6 +15,7 @@ import ( "os/exec" "strings" "testing" + "time" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" @@ -118,6 +119,8 @@ func (d *JSTestDriver) Start(t_ *testing.T, path string, width, height int) (cle ); err != nil { d.t.Fatal(err) } + // TODO(mvdan): synchronize with the app instead + time.Sleep(200 * time.Millisecond) return cleanups } @@ -136,6 +139,16 @@ func (d *JSTestDriver) Screenshot() image.Image { return img } +func (d *JSTestDriver) Click(x, y int) { + if err := chromedp.Run(d.ctx, + chromedp.MouseClickXY(float64(x), float64(y)), + ); err != nil { + d.t.Fatal(err) + } + // TODO(mvdan): synchronize with the app instead + time.Sleep(200 * time.Millisecond) +} + func TestJS(t *testing.T) { t.Parallel() diff --git a/cmd/gogio/testdata/red.go b/cmd/gogio/testdata/red.go index 95800405..0450cf3b 100644 --- a/cmd/gogio/testdata/red.go +++ b/cmd/gogio/testdata/red.go @@ -1,14 +1,16 @@ // SPDX-License-Identifier: Unlicense OR MIT -// A dead simple app that just paints the background red. +// A simple app used for gogio's end-to-end tests. package main import ( + "image" "image/color" "log" "gioui.org/app" "gioui.org/f32" + "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op/paint" @@ -25,10 +27,18 @@ func main() { } func loop(w *app.Window) error { - topLeft := color.RGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff} - topRight := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} - botLeft := color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff} - botRight := color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80} + topLeft := quarterWidget{ + color: color.RGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}, + } + topRight := quarterWidget{ + color: color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + } + botLeft := quarterWidget{ + color: color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + } + botRight := quarterWidget{ + color: color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80}, + } gtx := &layout.Context{ Queue: w.Queue(), @@ -39,32 +49,57 @@ func loop(w *app.Window) error { case system.DestroyEvent: return e.Err case system.FrameEvent: + gtx.Reset(e.Config, e.Size) rows := layout.Flex{Axis: layout.Vertical} r1 := rows.Flex(gtx, 0.5, func() { columns := layout.Flex{Axis: layout.Horizontal} - r1c1 := columns.Flex(gtx, 0.5, quarterWidget(gtx, topLeft)) - r1c2 := columns.Flex(gtx, 0.5, quarterWidget(gtx, topRight)) + r1c1 := columns.Flex(gtx, 0.5, func() { topLeft.Layout(gtx) }) + r1c2 := columns.Flex(gtx, 0.5, func() { topRight.Layout(gtx) }) columns.Layout(gtx, r1c1, r1c2) }) r2 := rows.Flex(gtx, 0.5, func() { columns := layout.Flex{Axis: layout.Horizontal} - r2c1 := columns.Flex(gtx, 0.5, quarterWidget(gtx, botLeft)) - r2c2 := columns.Flex(gtx, 0.5, quarterWidget(gtx, botRight)) + r2c1 := columns.Flex(gtx, 0.5, func() { botLeft.Layout(gtx) }) + r2c2 := columns.Flex(gtx, 0.5, func() { botRight.Layout(gtx) }) columns.Layout(gtx, r2c1, r2c2) }) rows.Layout(gtx, r1, r2) + e.Frame(gtx.Ops) } } } -func quarterWidget(gtx *layout.Context, clr color.RGBA) func() { - return func() { - paint.ColorOp{Color: clr}.Add(gtx.Ops) - paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{ - X: float32(gtx.Constraints.Width.Max), - Y: float32(gtx.Constraints.Height.Max), - }}}.Add(gtx.Ops) +// quarterWidget paints a quarter of the screen with one color. When clicked, it +// turns red, going back to its normal color when clicked again. +type quarterWidget struct { + color color.RGBA + + clicked bool +} + +var red = color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + +func (w *quarterWidget) Layout(gtx *layout.Context) { + if w.clicked { + paint.ColorOp{Color: red}.Add(gtx.Ops) + } else { + paint.ColorOp{Color: w.color}.Add(gtx.Ops) + } + paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{ + X: float32(gtx.Constraints.Width.Max), + Y: float32(gtx.Constraints.Height.Max), + }}}.Add(gtx.Ops) + + pointer.RectAreaOp{Rect: image.Rectangle{ + Max: image.Pt(gtx.Constraints.Width.Max, gtx.Constraints.Height.Max), + }}.Add(gtx.Ops) + pointer.InputOp{Key: w}.Add(gtx.Ops) + + for _, e := range gtx.Events(w) { + if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press { + w.clicked = !w.clicked + } } } diff --git a/cmd/gogio/x11_test.go b/cmd/gogio/x11_test.go index 58cab36b..34de3eca 100644 --- a/cmd/gogio/x11_test.go +++ b/cmd/gogio/x11_test.go @@ -30,6 +30,8 @@ import ( type X11TestDriver struct { t *testing.T + display string + // conn holds the connection to X. conn *xgbutil.XUtil } @@ -41,7 +43,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl // will only be using :0, so there's only a 0.001% chance of two // concurrent test runs to run into a conflict. rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - display := fmt.Sprintf(":%d", rnd.Intn(100000)+1) + d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1) var xprog string xflags := []string{ @@ -54,7 +56,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl xprog = "Xephyr" // nested X server as a window xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height)) } - xflags = append(xflags, display) + xflags = append(xflags, d.display) if _, err := exec.LookPath(xprog); err != nil { d.t.Skipf("%s needed to run with -headless=%t", xprog, *headless) } @@ -100,7 +102,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl // This socket path isn't terribly portable, but the xgb // library we use does the same, and we only really care // about Linux here. - socket := fmt.Sprintf("/tmp/.X11-unix/X%s", display[1:]) + socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:]) if _, err := os.Stat(socket); err == nil { break } @@ -125,7 +127,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl ctx, cancel := context.WithCancel(context.Background()) cmd := exec.CommandContext(ctx, bin) out := &bytes.Buffer{} - cmd.Env = append(os.Environ(), "DISPLAY="+display) + cmd.Env = append(os.Environ(), "DISPLAY="+d.display) cmd.Stdout = out cmd.Stderr = out if err := cmd.Start(); err != nil { @@ -146,7 +148,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl // Finally, connect to the X server. xgb.Logger.SetOutput(testLogWriter{d.t}) xgbutil.Logger.SetOutput(testLogWriter{d.t}) - conn, err := xgbutil.NewConnDisplay(display) + conn, err := xgbutil.NewConnDisplay(d.display) if err != nil { d.t.Fatal(err) } @@ -162,8 +164,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl }) // Wait for the gio app to render. - // TODO(mvdan): do this properly, e.g. via waiting for log lines - // from the gio program. + // TODO(mvdan): synchronize with the app instead time.Sleep(400 * time.Millisecond) return cleanups @@ -177,6 +178,27 @@ func (d *X11TestDriver) Screenshot() image.Image { return img } +func (d *X11TestDriver) xdotool(args ...interface{}) { + strs := make([]string, len(args)) + for i, arg := range args { + strs[i] = fmt.Sprint(arg) + } + cmd := exec.Command("xdotool", strs...) + cmd.Env = append(os.Environ(), "DISPLAY="+d.display) + if out, err := cmd.CombinedOutput(); err != nil { + d.t.Errorf("%s", out) + d.t.Fatal(err) + } +} + +func (d *X11TestDriver) Click(x, y int) { + d.xdotool("mousemove", x, y) + d.xdotool("click", "1") + + // TODO(mvdan): synchronize with the app instead + time.Sleep(200 * time.Millisecond) +} + func TestX11(t *testing.T) { t.Parallel()