| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package app_test |
| |
| import ( |
| "fmt" |
| "image" |
| "image/color" |
| _ "image/png" |
| "io/ioutil" |
| "net" |
| "os" |
| "os/exec" |
| "strings" |
| "testing" |
| "time" |
| |
| "golang.org/x/mobile/app/internal/apptest" |
| "golang.org/x/mobile/event/size" |
| ) |
| |
| // TestAndroidApp tests the lifecycle, event, and window semantics of a |
| // simple android app. |
| // |
| // Beyond testing the app package, the goal is to eventually have |
| // helper libraries that make tests like these easy to write. Hopefully |
| // having a user of such a fictional package will help illuminate the way. |
| func TestAndroidApp(t *testing.T) { |
| t.Skip("see issue #23835") |
| if _, err := exec.Command("which", "adb").CombinedOutput(); err != nil { |
| t.Skip("command adb not found, skipping") |
| } |
| devicesTxt, err := exec.Command("adb", "devices").CombinedOutput() |
| if err != nil { |
| t.Errorf("adb devices failed: %v: %v", err, devicesTxt) |
| } |
| deviceCount := 0 |
| for _, d := range strings.Split(strings.TrimSpace(string(devicesTxt)), "\n") { |
| if strings.Contains(d, "List of devices") { |
| continue |
| } |
| // TODO(crawshaw): I believe some unusable devices can appear in the |
| // list with note on them, but I cannot reproduce this right now. |
| deviceCount++ |
| } |
| if deviceCount == 0 { |
| t.Skip("no android devices attached") |
| } |
| |
| run(t, "gomobile", "version") |
| |
| origWD, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| tmpdir, err := ioutil.TempDir("", "app-test-") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmpdir) |
| |
| if err := os.Chdir(tmpdir); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Chdir(origWD) |
| |
| run(t, "gomobile", "install", "golang.org/x/mobile/app/internal/testapp") |
| |
| ln, err := net.Listen("tcp4", "localhost:0") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer ln.Close() |
| localaddr := fmt.Sprintf("tcp:%d", ln.Addr().(*net.TCPAddr).Port) |
| t.Logf("local address: %s", localaddr) |
| |
| exec.Command("adb", "reverse", "--remove", "tcp:"+apptest.Port).Run() // ignore failure |
| run(t, "adb", "reverse", "tcp:"+apptest.Port, localaddr) |
| |
| const ( |
| KeycodePower = "26" |
| KeycodeUnlock = "82" |
| ) |
| |
| run(t, "adb", "shell", "input", "keyevent", KeycodePower) |
| run(t, "adb", "shell", "input", "keyevent", KeycodeUnlock) |
| |
| const ( |
| rotationPortrait = "0" |
| rotationLandscape = "1" |
| ) |
| |
| rotate := func(rotation string) { |
| run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", "value:i:"+rotation) |
| } |
| |
| // turn off automatic rotation and start in portrait |
| run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:accelerometer_rotation", "--bind", "value:i:0") |
| rotate(rotationPortrait) |
| |
| // start testapp |
| run(t, |
| "adb", "shell", "am", "start", "-n", |
| "org.golang.testapp/org.golang.app.GoNativeActivity", |
| ) |
| |
| var conn net.Conn |
| connDone := make(chan struct{}) |
| go func() { |
| conn, err = ln.Accept() |
| connDone <- struct{}{} |
| }() |
| |
| select { |
| case <-time.After(5 * time.Second): |
| t.Fatal("timeout waiting for testapp to dial host") |
| case <-connDone: |
| if err != nil { |
| t.Fatalf("ln.Accept: %v", err) |
| } |
| } |
| defer conn.Close() |
| comm := &apptest.Comm{ |
| Conn: conn, |
| Fatalf: t.Fatalf, |
| Printf: t.Logf, |
| } |
| |
| var pixelsPerPt float32 |
| var orientation size.Orientation |
| |
| comm.Recv("hello_from_testapp") |
| comm.Send("hello_from_host") |
| comm.Recv("lifecycle_visible") |
| comm.Recv("size", &pixelsPerPt, &orientation) |
| if pixelsPerPt < 0.1 { |
| t.Fatalf("bad pixelsPerPt: %f", pixelsPerPt) |
| } |
| |
| // A single paint event is sent when the lifecycle enters |
| // StageVisible, and after the end of a touch event. |
| var color string |
| comm.Recv("paint", &color) |
| // Ignore the first paint color, it may be slow making it to the screen. |
| |
| rotate(rotationLandscape) |
| comm.Recv("size", &pixelsPerPt, &orientation) |
| if want := size.OrientationLandscape; orientation != want { |
| t.Errorf("want orientation %d, got %d", want, orientation) |
| } |
| |
| var x, y int |
| var ty string |
| |
| tap(t, 50, 260) |
| comm.Recv("touch", &ty, &x, &y) |
| if ty != "begin" || x != 50 || y != 260 { |
| t.Errorf("want touch begin(50, 260), got %s(%d,%d)", ty, x, y) |
| } |
| comm.Recv("touch", &ty, &x, &y) |
| if ty != "end" || x != 50 || y != 260 { |
| t.Errorf("want touch end(50, 260), got %s(%d,%d)", ty, x, y) |
| } |
| |
| comm.Recv("paint", &color) |
| if gotColor := currentColor(t); color != gotColor { |
| t.Errorf("app reports color %q, but saw %q", color, gotColor) |
| } |
| |
| rotate(rotationPortrait) |
| comm.Recv("size", &pixelsPerPt, &orientation) |
| if want := size.OrientationPortrait; orientation != want { |
| t.Errorf("want orientation %d, got %d", want, orientation) |
| } |
| |
| tap(t, 50, 260) |
| comm.Recv("touch", &ty, &x, &y) // touch begin |
| comm.Recv("touch", &ty, &x, &y) // touch end |
| comm.Recv("paint", &color) |
| if gotColor := currentColor(t); color != gotColor { |
| t.Errorf("app reports color %q, but saw %q", color, gotColor) |
| } |
| |
| // TODO: lifecycle testing (NOTE: adb shell input keyevent 4 is the back button) |
| } |
| |
| func currentColor(t *testing.T) string { |
| file := fmt.Sprintf("app-screen-%d.png", time.Now().Unix()) |
| |
| run(t, "adb", "shell", "screencap", "-p", "/data/local/tmp/"+file) |
| run(t, "adb", "pull", "/data/local/tmp/"+file) |
| run(t, "adb", "shell", "rm", "/data/local/tmp/"+file) |
| defer os.Remove(file) |
| |
| f, err := os.Open(file) |
| if err != nil { |
| t.Errorf("currentColor: cannot open screencap: %v", err) |
| return "" |
| } |
| m, _, err := image.Decode(f) |
| if err != nil { |
| t.Errorf("currentColor: cannot decode screencap: %v", err) |
| return "" |
| } |
| var center color.Color |
| { |
| b := m.Bounds() |
| x, y := b.Min.X+(b.Max.X-b.Min.X)/2, b.Min.Y+(b.Max.Y-b.Min.Y)/2 |
| center = m.At(x, y) |
| } |
| r, g, b, _ := center.RGBA() |
| switch { |
| case r == 0xffff && g == 0x0000 && b == 0x0000: |
| return "red" |
| case r == 0x0000 && g == 0xffff && b == 0x0000: |
| return "green" |
| case r == 0x0000 && g == 0x0000 && b == 0xffff: |
| return "blue" |
| default: |
| return fmt.Sprintf("indeterminate: %v", center) |
| } |
| } |
| |
| func tap(t *testing.T, x, y int) { |
| run(t, "adb", "shell", "input", "tap", fmt.Sprintf("%d", x), fmt.Sprintf("%d", y)) |
| } |
| |
| func run(t *testing.T, cmdName string, arg ...string) { |
| cmd := exec.Command(cmdName, arg...) |
| t.Log(strings.Join(cmd.Args, " ")) |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| t.Fatalf("%s %v: %s", strings.Join(cmd.Args, " "), err, out) |
| } |
| } |