From 7fdfe04658d16a05ebcb016ee964a7263eb74802 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 8 Oct 2026 10:57:38 +0800 Subject: [PATCH] fix(android): reject address editor hint values (#366) --- .../automation/GoAutoAccessibilityService.kt | 2 + .../automation/PddProductDetailCollector.kt | 3 + .../automation/PurchaseLiveAutomation.kt | 11 ++- .../agent/PurchaseLiveAutomationTest.kt | 97 ++++++++++++++++++- 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt index be6c6f4..4ff2c6f 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt @@ -248,6 +248,8 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto checked = node.isChecked, enabled = node.isEnabled, visible = node.isVisibleToUser, + hintText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) node.hintText?.toString() else null, + showingHintText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) node.isShowingHintText else null, ) for (index in 0 until node.childCount) { node.getChild(index)?.let { snapshot(it, "$path/$index", path) } diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt index 4f49eef..d298d72 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt @@ -27,6 +27,9 @@ data class SnapshotNode( val checked: Boolean, val enabled: Boolean, val visible: Boolean, + // In-memory accessibility metadata; not part of result or diagnostic payloads. + val hintText: String? = null, + val showingHintText: Boolean? = null, ) { val label: String get() = text?.trim().takeUnless { it.isNullOrEmpty() } ?: contentDescription?.trim().orEmpty() diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt index ff7fc60..c6f98f3 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt @@ -510,7 +510,7 @@ class PurchaseLiveAutomation( if (modify.size != 1) fail("PURCHASE_ADDRESS_EDIT_AMBIGUOUS", "没有找到唯一的地址修改按钮,未创建订单") click(modify.single(), "修改地址") val editor = waitForInitialAddressEditor() - val current = editor.label.trim() + val current = editor.text.orEmpty().trim() val body = current.split(Regex("[-_]"), limit = 2).firstOrNull()?.trim().orEmpty() if (body.isBlank()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "当前详细地址无效,未创建订单") val expected = body + suffix @@ -537,6 +537,10 @@ class PurchaseLiveAutomation( /** Initial readiness only; return the same sampled node for the original value and fresh input anchor. */ private fun waitForInitialAddressEditor(): SnapshotNode { + fun isHint(editor: SnapshotNode): Boolean { + val hint = editor.hintText?.trim().orEmpty() + return editor.showingHintText == true || (hint.isNotEmpty() && editor.text?.trim() == hint) + } var titleSeen = false var structural = emptyList() var readyCount = 0 @@ -555,7 +559,7 @@ class PurchaseLiveAutomation( editor.bounds.left >= label.bounds.left } }.distinctBy { it.path } - readyCount = structural.count { it.enabled && it.label.isNotBlank() } + readyCount = structural.count { it.enabled && !it.text.isNullOrBlank() && !isHint(it) } if (structural.size == 1 && readyCount == 1) return structural.single() pause(200) } @@ -564,7 +568,8 @@ class PurchaseLiveAutomation( structural.isEmpty() -> "editor_missing" structural.size > 1 -> "editor_ambiguous" !structural.single().enabled -> "editor_disabled" - else -> "editor_empty" + structural.single().text.isNullOrBlank() -> "editor_empty" + else -> "editor_hint" } val diagnostic = "stage=address_editor_ready;reason=$reason;titleSeen=$titleSeen;structural=${structural.size};ready=$readyCount" if (!titleSeen) fail("PURCHASE_ADDRESS_EDIT_TIMEOUT", "地址编辑页面打开超时,未创建订单 [$diagnostic]") diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt index 45da654..ba0dbe0 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt @@ -233,6 +233,101 @@ class PurchaseLiveAutomationTest { } } + @Test + fun `address editor description cannot substitute for empty text`() { + for (text in listOf(null, "", " ")) { + assertAddressEditorTimeout("editor_empty", 1, 0) { snapshot, _ -> + snapshot.changeAddressEditor { it.copy(text = text, contentDescription = "合成说明占位标记") } + } + } + } + + @Test + fun `address editor waits until hint is replaced by real text`() { + assertDelayedAddressEditor { snapshot, sample -> + snapshot.changeAddressEditor { + it.copy( + text = if (sample <= 4) "合成提示占位标记" else it.text, + hintText = "合成提示占位标记", + showingHintText = sample <= 4, + ) + } + } + } + + @Test + fun `address editor persistent hint text fails after the full budget`() { + assertAddressEditorTimeout("editor_hint", 1, 0) { snapshot, _ -> + snapshot.changeAddressEditor { + it.copy(text = "合成提示占位标记", hintText = "合成提示占位标记", showingHintText = true) + } + } + } + + @Test + fun `address editor rejects text equal to trimmed hint despite false or absent showing flag`() { + for (showing in listOf(false, null)) { + assertAddressEditorTimeout("editor_hint", 1, 0) { snapshot, _ -> + snapshot.changeAddressEditor { + it.copy(text = " 合成提示占位标记 ", hintText = " 合成提示占位标记 ", showingHintText = showing) + } + } + } + } + + @Test + fun `address editor showing hint flag blocks nonmatching or missing hint text`() { + for (hint in listOf(null, "", "合成提示占位标记")) { + assertAddressEditorTimeout("editor_hint", 1, 0) { snapshot, _ -> + snapshot.changeAddressEditor { it.copy(hintText = hint, showingHintText = true) } + } + } + } + + @Test + fun `address editor empty text remains empty even when hint and description are present`() { + assertAddressEditorTimeout("editor_empty", 1, 0) { snapshot, _ -> + snapshot.changeAddressEditor { + it.copy(text = " ", contentDescription = "合成说明占位标记", hintText = "合成提示占位标记", showingHintText = true) + } + } + } + + @Test + fun `address editor missing hint metadata remains compatible and uses actual text`() { + val driver = LiveDriver(initialEditFrame = { snapshot, _ -> + snapshot.changeAddressEditor { + assertEquals(null, it.hintText) + assertEquals(null, it.showingHintText) + it.copy(text = " 合成真实地址-old ", contentDescription = "合成说明占位标记") + } + }) + + val proof = PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366") + + assertEquals("合成真实地址_cg366", proof.expectedAddress) + assertEquals(1, driver.initialEditCaptures) + assertTrue(driver.lastInputTarget === driver.lastInitialEditSnapshot?.nodes?.single { it.path == "editor-address" }) + assertAddressEditorActions(driver, saved = true) + } + + @Test + fun `address editor accepts actual text with absent false or blank hint metadata`() { + for (showing in listOf(null, false)) { + for (hint in listOf(null, "", " ", "合成提示占位标记")) { + val driver = LiveDriver(initialEditFrame = { snapshot, _ -> + snapshot.changeAddressEditor { it.copy(hintText = hint, showingHintText = showing) } + }) + + val proof = PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366") + + assertTrue(proof.expectedAddress.endsWith("_cg366")) + assertEquals(1, driver.initialEditCaptures) + assertAddressEditorActions(driver, saved = true) + } + } + } + @Test fun `address editor waits for an enabled input`() { assertDelayedAddressEditor { snapshot, sample -> @@ -436,7 +531,7 @@ class PurchaseLiveAutomationTest { assertEquals(50, driver.initialEditCaptures) assertEquals(listOf(500L) + List(50) { 200L }, editPauses) val emitted = error.message.orEmpty() + diagnostics.joinToString() - listOf("广东", "骏景", "测试收货人", "13800000000", "editor-address", "_cg366").forEach { + listOf("广东", "骏景", "测试收货人", "13800000000", "editor-address", "_cg366", "合成提示占位标记", "合成说明占位标记").forEach { assertFalse("Sensitive fixture value in readiness diagnostics", emitted.contains(it)) } assertAddressEditorActions(driver, saved = false)