Files
sanderling/internal/driver/ioscompanion/device_test.go
T

198 lines
5.6 KiB
Go

package ioscompanion
import (
"bytes"
"context"
"io"
"net"
"os/exec"
"testing"
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport"
)
// startLoopbackListener accepts and immediately closes connections so
// waitForListener succeeds against a real address without a real runner.
func startLoopbackListener(t *testing.T) string {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { listener.Close() })
go func() {
for {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
return
}
_ = conn.Close()
}
}()
return listener.Addr().String()
}
func testDeviceOptions(address string, companion transport.Companion) DeviceOptions {
return DeviceOptions{
HardwareUDID: "00008140-HW",
CoreDeviceID: "CORE-1",
BundleID: "com.example.app",
Output: &bytes.Buffer{},
spawnRunner: func(context.Context, string) (*exec.Cmd, error) { return &exec.Cmd{}, nil },
startTunnel: func(context.Context, string, string, string) (io.Closer, error) { return io.NopCloser(nil), nil },
dialRunner: func(string) (transport.Companion, error) { return companion, nil },
pickAddress: func() (string, error) { return address, nil },
}
}
func newDeviceCompanion() *fakeTextEditingCompanion {
companion := &fakeTextEditingCompanion{}
companion.accessibilityJSON = "[]"
companion.describe = transport.ScreenDescription{WidthPoints: 393, HeightPoints: 852}
return companion
}
func TestNewDeviceWiresRunnerOnlyMode(t *testing.T) {
address := startLoopbackListener(t)
companion := newDeviceCompanion()
d, err := NewDevice(context.Background(), testDeviceOptions(address, companion))
if err != nil {
t.Fatalf("NewDevice: %v", err)
}
defer d.Close()
if !d.deviceMode {
t.Fatal("deviceMode must be true")
}
if d.hybrid {
t.Fatal("device mode must not be hybrid")
}
if d.runnerClient != nil {
t.Fatal("device mode must leave runnerClient nil so InputText avoids the hybrid HID chord")
}
if d.companion != companion {
t.Fatal("d.companion must be the runner dialed over the tunnel")
}
if d.coreDeviceID != "CORE-1" {
t.Fatalf("coreDeviceID = %q, want CORE-1", d.coreDeviceID)
}
if d.screenWidth != 393 || d.screenHeight != 852 {
t.Fatalf("screen = %dx%d, want 393x852", d.screenWidth, d.screenHeight)
}
}
func TestNewDeviceRequiresIdentifiers(t *testing.T) {
if _, err := NewDevice(context.Background(), DeviceOptions{CoreDeviceID: "x"}); err == nil {
t.Fatal("missing HardwareUDID must error")
}
if _, err := NewDevice(context.Background(), DeviceOptions{HardwareUDID: "x"}); err == nil {
t.Fatal("missing CoreDeviceID must error")
}
}
func TestDeviceInputTextUsesNativeEditorNotKeyboardHID(t *testing.T) {
address := startLoopbackListener(t)
companion := newDeviceCompanion()
d, err := NewDevice(context.Background(), testDeviceOptions(address, companion))
if err != nil {
t.Fatal(err)
}
defer d.Close()
if err := d.InputText(context.Background(), "héllo 🌟"); err != nil {
t.Fatal(err)
}
if len(companion.inputTexts) != 1 || companion.inputTexts[0] != "héllo 🌟" {
t.Fatalf("inputTexts = %v, want native replace of the text", companion.inputTexts)
}
for _, call := range companion.recorded() {
if call == "hid" {
t.Fatal("device text must go through the native editor, never a keyboard HID chord")
}
}
}
func TestDeviceGesturesRouteTouchHIDToRunner(t *testing.T) {
address := startLoopbackListener(t)
companion := newDeviceCompanion()
d, err := NewDevice(context.Background(), testDeviceOptions(address, companion))
if err != nil {
t.Fatal(err)
}
defer d.Close()
if err := d.DoubleTap(context.Background(), 10, 20); err != nil {
t.Fatal(err)
}
if indexOf(companion.recorded(), "hid") < 0 {
t.Fatalf("gesture must send touch HID to the runner; got %v", companion.recorded())
}
}
func TestDeviceEraseAndPressKeyRouteThroughEditor(t *testing.T) {
address := startLoopbackListener(t)
companion := newDeviceCompanion()
d, err := NewDevice(context.Background(), testDeviceOptions(address, companion))
if err != nil {
t.Fatal(err)
}
defer d.Close()
if err := d.EraseText(context.Background(), 4); err != nil {
t.Fatal(err)
}
if err := d.PressKey(context.Background(), "enter"); err != nil {
t.Fatal(err)
}
if len(companion.eraseCounts) != 1 || companion.eraseCounts[0] != 4 {
t.Fatalf("eraseCounts = %v, want [4]", companion.eraseCounts)
}
if len(companion.pressedKeys) != 1 || companion.pressedKeys[0] != "enter" {
t.Fatalf("pressedKeys = %v, want [enter]", companion.pressedKeys)
}
}
func TestDeviceClearStateWithoutAppPathWarnsOnce(t *testing.T) {
output := &bytes.Buffer{}
d := &Driver{output: output, deviceMode: true}
d.resetContainer = d.deviceResetContainerUnsupported
for i := 0; i < 2; i++ {
if err := d.deviceResetContainerUnsupported(context.Background()); err != nil {
t.Fatal(err)
}
}
if got := bytes.Count(output.Bytes(), []byte("requires --ios-app-path")); got != 1 {
t.Fatalf("warning emitted %d times, want once", got)
}
}
type recordingCloser struct{ closed bool }
func (c *recordingCloser) Close() error {
c.closed = true
return nil
}
func TestDeviceCloseStopsRunnerAndTunnel(t *testing.T) {
d := &Driver{output: &bytes.Buffer{}}
runner := exec.Command("sleep", "30")
if err := runner.Start(); err != nil {
t.Fatal(err)
}
tunnel := &recordingCloser{}
d.runnerChild = runner
d.tunnel = tunnel
d.Close()
if d.runnerChild != nil || d.tunnel != nil {
t.Fatal("Close must clear the runner child and the tunnel")
}
if runner.ProcessState == nil {
t.Fatal("Close must reap the runner session child")
}
if !tunnel.closed {
t.Fatal("Close must close the usbmux tunnel")
}
}