From 71219d71daca07d10aa11c6e13f77a3f55009989 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 19 Aug 2026 18:51:07 +0530 Subject: [PATCH] fix(sidecar): state the fact on a field named by its hint alone collectTextFields matched on class only, so a node the go side calls editable off its hintText was left unstated and its typed value redacted. --- .../dev/sanderling/sidecar/DriverBackend.kt | 14 ++++++++++++-- .../dev/sanderling/sidecar/SecureFactsTest.kt | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index 6eadb11..50cdb2c 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -1017,12 +1017,18 @@ internal fun secureFactsFromXml(xml: String): Map { private fun secureFactKey(id: String, bounds: String) = "$id@$bounds" +// A text field is what the Go side calls editable off the same two attributes +// (internal/hierarchy): stating the fact on a narrower set would leave fields +// the rest of the system treats as typeable answering for nothing. private fun collectTextFields( node: com.fasterxml.jackson.databind.JsonNode, into: MutableList, ) { if (node is com.fasterxml.jackson.databind.node.ObjectNode && - nodeAttribute(node, "class").contains("EditText") + ( + nodeAttribute(node, "class").contains("EditText") || + nodeAttribute(node, "hintText").isNotEmpty() + ) ) { into.add(node) } @@ -1256,7 +1262,11 @@ class MaestroDriverBackend(private val serial: String?) : DriverBackend { // can tell the runner the gesture reached nothing. private fun requireOnScreen(x: Int, y: Int) { val cached = extent - if (cached != null && !offScreen(x, y, cached.first, cached.second)) return + if (cached != null && + !offScreen(x, y, cached.first, cached.second) + ) { + return + } val info = driver.deviceInfo() val fresh = Pair(info.widthPixels, info.heightPixels) extent = fresh diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt index c20ab61..e870163 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt @@ -79,6 +79,23 @@ class SecureFactsTest { ) } + // The Go side calls a node editable off its class or its hint, and a field + // it will happily type into has to be a field this can speak for. + @Test fun aFieldNamedByItsHintAloneIsStatedToo() { + val hinted = """ + {"attributes":{"bounds":"[0,0,1080,2340]"},"children":[ + {"attributes":{"resource-id":"Search","class":"android.view.View","hintText":"Search","bounds":"[10,10,200,50]"},"children":[]} + ]} + """ + val xml = """ + + + + """ + + assertEquals(false, secureOf(withSecureFacts(hinted) { xml }, "Search")) + } + // Unstated means "may be a credential" downstream. Every way this can fail // has to land there rather than on a false "not secure". @Test fun aFactThatCannotBeReadIsLeftUnstated() {