diff --git a/sidecar/build.gradle.kts b/sidecar/build.gradle.kts index 698eeaa..d39585d 100644 --- a/sidecar/build.gradle.kts +++ b/sidecar/build.gradle.kts @@ -26,6 +26,8 @@ val maestroVersion = "1.40.0" dependencies { implementation("dev.mobile:maestro-client:$maestroVersion") implementation("dev.mobile:maestro-utils:$maestroVersion") + implementation("dev.mobile:maestro-ios:$maestroVersion") + implementation("dev.mobile:maestro-ios-driver:$maestroVersion") implementation("io.grpc:grpc-netty-shaded:$grpcVersion") implementation("io.grpc:grpc-protobuf:$grpcVersion") diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index 62a0a21..be84c3b 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -1,7 +1,7 @@ package dev.sanderling.sidecar interface DriverBackend { - fun launch(bundleId: String, clearState: Boolean) + fun launch(bundleId: String, clearState: Boolean, env: Map = emptyMap()) fun terminate(bundleId: String) fun tap(x: Int, y: Int) fun tapSelector(selector: String) @@ -131,7 +131,7 @@ class StubDriverBackend(private val platform: String) : DriverBackend { @Volatile var lastInputText: String? = null private set - override fun launch(bundleId: String, clearState: Boolean) { + override fun launch(bundleId: String, clearState: Boolean, env: Map) { launchCount++ lastBundleId = bundleId if (clearState) { @@ -386,9 +386,9 @@ class MaestroDriverBackend(private val serial: String?) : DriverBackend { driver.open() } - override fun launch(bundleId: String, clearState: Boolean) { + override fun launch(bundleId: String, clearState: Boolean, env: Map) { if (clearState) driver.clearAppState(bundleId) - driver.launchApp(bundleId, emptyMap(), java.util.UUID.randomUUID()) + driver.launchApp(bundleId, env, java.util.UUID.randomUUID()) } override fun terminate(bundleId: String) = driver.stopApp(bundleId) @@ -486,6 +486,80 @@ private fun pngHeight(bytes: ByteArray): Int { (bytes[22].toInt() and 0xFF shl 8) or (bytes[23].toInt() and 0xFF) } +class IosDriverBackend(private val udid: String) : DriverBackend { + private val driver: maestro.drivers.IOSDriver + + init { + val httpClient = xcuitest.api.OkHttpClientInstance.get() + val metrics = maestro.utils.NoOpMetrics() + val wdaPort = maestro.utils.SocketUtils.nextFreePort(22000, 23000) + val installer = xcuitest.installer.LocalXCTestInstaller( + udid, + "localhost", + false, + wdaPort, + metrics, + httpClient, + false, + false, + ) + val xcTestDriverClient = xcuitest.XCTestDriverClient(installer, httpClient, false) + val xcTestDevice = ios.xctest.XCTestIOSDevice(udid, xcTestDriverClient) { emptySet() } + val simctlDevice = ios.simctl.SimctlIOSDevice(udid) + val device = ios.LocalIOSDevice(udid, xcTestDevice, simctlDevice, maestro.utils.NoopInsights) + driver = maestro.drivers.IOSDriver(device, maestro.utils.NoopInsights, metrics) + driver.open() + } + + override fun launch(bundleId: String, clearState: Boolean, env: Map) { + runCatching { driver.stopApp(bundleId) } + if (clearState) driver.clearAppState(bundleId) + driver.launchApp(bundleId, env, java.util.UUID.randomUUID()) + } + + override fun terminate(bundleId: String) = driver.stopApp(bundleId) + + override fun tap(x: Int, y: Int) = driver.tap(maestro.Point(x, y)) + + override fun tapSelector(selector: String) { + val root = driver.contentDescriptor(false) + val bounds = findBoundsBySelector(root, selector) ?: return + driver.tap(maestro.Point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2)) + } + + override fun inputText(text: String) = driver.inputText(text) + + override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) = + driver.swipe(maestro.Point(fromX, fromY), maestro.Point(toX, toY), maxOf(durationMillis, 250L)) + + override fun pressKey(key: String) { + StubDriverBackend.KEY_MAP[key]?.let { keyCode -> + keyCodeToMaestro(keyCode)?.let { driver.pressKey(it) } + } + } + + override fun screenshot(): Triple { + val buf = okio.Buffer() + driver.takeScreenshot(buf, false) + val bytes = buf.readByteArray() + return Triple(bytes, pngWidth(bytes), pngHeight(bytes)) + } + + override fun hierarchy(): String = + com.fasterxml.jackson.module.kotlin.jacksonObjectMapper() + .writeValueAsString(driver.contentDescriptor(false)) + + override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List = emptyList() + + override fun waitForIdle(durationMillis: Long) { + driver.waitForAppToSettle(null, null, durationMillis.toInt()) + } + + override fun healthy() = runCatching { driver.contentDescriptor(false); true }.getOrElse { false } + + override fun metrics(bundleId: String) = MetricsSample(0.0, 0L, 0L) +} + private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? { return when (adbKeyCode) { "KEYCODE_BACK" -> maestro.KeyCode.BACK diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt index 8a2f19b..30f4989 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt @@ -30,7 +30,7 @@ class DriverService( override fun launch(request: LaunchRequest, responseObserver: StreamObserver) { runRpc(responseObserver) { - backend.launch(request.bundleId, request.clearState) + backend.launch(request.bundleId, request.clearState, request.envMap) launchedBundleId.set(request.bundleId) Empty.getDefaultInstance() } @@ -150,7 +150,8 @@ class DriverService( observer.onNext(block()) observer.onCompleted() } catch (cause: Exception) { - observer.onError(cause) + observer.onError(io.grpc.Status.INTERNAL.withDescription(cause.toString()) + .withCause(cause).asRuntimeException()) } } diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt index 929ac6d..4aeed6c 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt @@ -44,11 +44,14 @@ fun main(arguments: Array) { val serial = arguments.indexOf("--serial").let { index -> if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null } + val udid = arguments.indexOf("--udid").let { index -> + if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null + } - val backend: DriverBackend = if (platform == "android") { - MaestroDriverBackend(serial) - } else { - StubDriverBackend(platform) + val backend: DriverBackend = when (platform) { + "android" -> MaestroDriverBackend(serial) + "ios" -> IosDriverBackend(udid ?: error("--udid required for ios platform")) + else -> StubDriverBackend(platform) } val service = DriverService(platform = platform, backend = backend) val server = SidecarServer(port, service)