// SPDX-License-Identifier: Unlicense OR MIT package main_test import ( "flag" "image" "image/color" "testing" "time" ) var raceEnabled = false var headless = flag.Bool("headless", true, "run end-to-end tests in headless mode") const appid = "localhost.gogio.endtoend" // TestDriver is implemented by each of the platforms we can run end-to-end // tests on. None of its methods return any errors, as the errors are directly // reported to testing.T via methods like Fatal. type TestDriver interface { initBase(*testing.T) // Start opens the Gio app found at path. The driver should attempt to // run the app with the given width and height, and the platform's // background should be white. // // When the function returns, the gio app must be ready to use on the // platform, with its initial frame fully drawn. Start(path string, width, height int) // 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. It returns when the next frame is // fully drawn. Click(x, y int) } type driverBase struct { *testing.T frameNotifs chan bool } func (d *driverBase) initBase(t *testing.T) { d.T = t d.frameNotifs = make(chan bool, 1) } func TestEndToEnd(t *testing.T) { if testing.Short() { t.Skipf("end-to-end tests tend to be slow") } t.Parallel() // Keep this list local, to not reuse TestDriver objects. subtests := []struct { name string driver TestDriver }{ {"X11", &X11TestDriver{}}, {"Wayland", &WaylandTestDriver{}}, {"JS", &JSTestDriver{}}, {"Android", &AndroidTestDriver{}}, } for _, subtest := range subtests { t.Run(subtest.name, func(t *testing.T) { subtest := subtest // copy the changing loop variable t.Parallel() runEndToEndTest(t, subtest.driver) }) } } func runEndToEndTest(t *testing.T, driver TestDriver) { driver.initBase(t) size := image.Point{X: 800, Y: 600} t.Log("starting driver and gio app") driver.Start("testdata/red.go", 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. wantColors := func(topLeft, topRight, botLeft, botRight color.RGBA) { t.Helper() img := driver.Screenshot() size = img.Bounds().Size() { minX, minY := 5, 5 maxX, maxY := (size.X/2)-5, (size.Y/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 := (size.X/2)+5, 5 maxX, maxY := size.X-5, (size.Y/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, (size.Y/2)+5 maxX, maxY := (size.X/2)-5, size.Y-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 := (size.X/2)+5, (size.Y/2)+5 maxX, maxY := size.X-5, size.Y-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) } } 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} // These are the four colors at the beginning. t.Log("taking initial screenshot") wantColors(beef, white, black, gray) // TODO(mvdan): implement this properly in the Wayland driver; swaymsg // almost works to automate clicks, but the button presses end up in the // wrong coordinates. if _, ok := driver.(*WaylandTestDriver); ok { return } // Click the first and last sections to turn them red. t.Log("clicking twice and taking another screenshot") driver.Click(1*(size.X/4), 1*(size.Y/4)) driver.Click(3*(size.X/4), 3*(size.Y/4)) wantColors(red, white, black, red) } func wantColor(t *testing.T, img image.Image, x, y int, want color.Color) { t.Helper() 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) } } func (d *driverBase) waitForFrame() { d.Helper() // Unfortunately, there isn't a way to select on a test failing, since // testing.T doesn't have anything like a context or a "done" channel. // // We can't let selects block forever, since the default -test.timeout // is ten minutes - far too long for tests that take seconds. // // For now, a static short timeout is better than nothing. 2s is plenty // for our simple test app to render on any device. select { case <-d.frameNotifs: case <-time.After(5 * time.Second): d.Fatalf("timed out waiting for a frame to be ready") } }