From 5ede139945279b7a492c8a508177e00018c8ca68 Mon Sep 17 00:00:00 2001 From: PJ Date: Fri, 5 Jun 2026 17:40:09 +0530 Subject: [PATCH] feat(sidecar): EraseText backend support on android and ios --- .../dev/sanderling/sidecar/DriverBackend.kt | 15 +++++++++++++++ .../dev/sanderling/sidecar/DriverService.kt | 8 ++++++++ .../dev/sanderling/sidecar/DriverServiceTest.kt | 9 +++++++++ .../dev/sanderling/sidecar/InputTextTest.kt | 12 ++++++++++++ .../dev/sanderling/sidecar/SnapshotHandlerTest.kt | 1 + 5 files changed, 45 insertions(+) diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index af8c466..a5c599e 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -6,6 +6,7 @@ interface DriverBackend { fun tap(x: Int, y: Int) fun tapSelector(selector: String) fun inputText(text: String) + fun eraseText(characterCount: Int) fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) fun pressKey(key: String) fun longPress(x: Int, y: Int) @@ -402,6 +403,16 @@ class StubDriverBackend( runAdb(listOf("shell", "input", "text", escapeForAdbInputText(text))) } + @Volatile var lastEraseCharacterCount: Int? = null + private set + + override fun eraseText(characterCount: Int) { + lastEraseCharacterCount = characterCount + repeat(characterCount) { + runAdb(listOf("shell", "input", "keyevent", "KEYCODE_DEL")) + } + } + @Volatile var lastSwipe: SwipeRecord? = null private set @Volatile var lastKey: String? = null @@ -531,6 +542,8 @@ class MaestroDriverBackend(private val serial: String?) : DriverBackend { override fun inputText(text: String) = driver.inputText(text) + override fun eraseText(characterCount: Int) = driver.eraseText(characterCount) + 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)) @@ -732,6 +745,8 @@ class IosDriverBackend(private val udid: String) : DriverBackend { override fun inputText(text: String) = withReconnect { driver.inputText(text) } + override fun eraseText(characterCount: Int) = withReconnect { driver.eraseText(characterCount) } + override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) = withReconnect { driver.swipe(maestro.Point(fromX, fromY), maestro.Point(toX, toY), maxOf(durationMillis, 250L)) } diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt index e2e86b7..421a205 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString import dev.sanderling.driver.v1.DriverGrpc import dev.sanderling.driver.v1.Duration import dev.sanderling.driver.v1.Empty +import dev.sanderling.driver.v1.EraseTextRequest import dev.sanderling.driver.v1.HealthStatus import dev.sanderling.driver.v1.HierarchyJSON import dev.sanderling.driver.v1.Image @@ -74,6 +75,13 @@ class DriverService( } } + override fun eraseText(request: EraseTextRequest, responseObserver: StreamObserver) { + runRpc(responseObserver) { + backend.eraseText(request.characterCount) + Empty.getDefaultInstance() + } + } + override fun swipe(request: SwipeRequest, responseObserver: StreamObserver) { runRpc(responseObserver) { val from = request.from diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt index 36e92e4..073a322 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt @@ -3,6 +3,7 @@ package dev.sanderling.sidecar import dev.sanderling.driver.v1.DriverGrpc import dev.sanderling.driver.v1.Duration import dev.sanderling.driver.v1.Empty +import dev.sanderling.driver.v1.EraseTextRequest import dev.sanderling.driver.v1.LaunchRequest import dev.sanderling.driver.v1.Point import dev.sanderling.driver.v1.PressKeyRequest @@ -82,6 +83,14 @@ class DriverServiceTest { assertEquals("hello world", backend.lastInputText) } + @Test fun eraseTextForwardsCharacterCount() { + val backend = StubDriverBackend("android") + val client = newClient(backend) + + client.eraseText(EraseTextRequest.newBuilder().setCharacterCount(11).build()) + assertEquals(11, backend.lastEraseCharacterCount) + } + @Test fun screenshotReturnsBackendBytes() { val backend = object : DriverBackend by StubDriverBackend("android") { override fun screenshot(): Triple = Triple(byteArrayOf(1, 2, 3), 1080, 2340) diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/InputTextTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/InputTextTest.kt index a9b2409..55f928c 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/InputTextTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/InputTextTest.kt @@ -15,6 +15,18 @@ class InputTextTest { assertEquals(listOf(listOf("shell", "input", "text", "Emergency%sFund")), commands) } + @Test fun eraseTextSendsOneDeleteKeyPerCharacter() { + val commands = mutableListOf>() + val backend = StubDriverBackend("android") { commands.add(it) } + + backend.eraseText(3) + + assertEquals( + List(3) { listOf("shell", "input", "keyevent", "KEYCODE_DEL") }, + commands, + ) + } + @Test fun escapeForAdbInputTextSubstitutesSpaces() { assertEquals("hello%sworld", StubDriverBackend.escapeForAdbInputText("hello world")) } diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SnapshotHandlerTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SnapshotHandlerTest.kt index 417896d..13c6e45 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SnapshotHandlerTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SnapshotHandlerTest.kt @@ -60,6 +60,7 @@ class SnapshotHandlerTest { override fun tap(x: Int, y: Int) {} override fun tapSelector(selector: String) {} override fun inputText(text: String) {} + override fun eraseText(characterCount: Int) {} override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {} override fun pressKey(key: String) {} override fun longPress(x: Int, y: Int) {}