diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3ded304..5db2a69 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "cn.ilapage.goauto.agent" minSdk = 23 targetSdk = 34 - versionCode = 77 - versionName = "0.9.64" + versionCode = 78 + versionName = "0.9.65" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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 4bdcab8..42259ef 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 @@ -524,9 +524,10 @@ class PurchaseLiveAutomation( if (!isPurchaseConfirmationPanel(snapshot)) { fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后采购面板发生变化,未创建订单") } - val panels = purchasePanelScrollTargets(snapshot) + val panels = savedAddressPanelScrollTargets(snapshot) if (panels.size != 1) { - fail("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", "地址保存后没有找到唯一的规格面板滚动区域,未创建订单") + val reason = if (panels.isEmpty()) "未找到可用的规格面板滚动区域" else "找到多个规格面板滚动区域" + fail("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", "地址保存后${reason},未创建订单") } val panel = panels.single() val signature = viewportSignature(snapshot, panel) @@ -594,6 +595,20 @@ class PurchaseLiveAutomation( fail(code, message) } + private fun savedAddressPanelScrollTargets(snapshot: UiSnapshot): List { + val panel = PddScreenParser.parse(snapshot, PurchaseRehearsalExecutor.DEFAULT_COLLECTOR, "", null) + .specPanelContainer ?: return purchasePanelScrollTargets(snapshot) + val screenWidth = snapshot.nodes.maxOfOrNull { it.bounds.right }?.coerceAtLeast(1) ?: 1 + val screenHeight = snapshot.nodes.maxOfOrNull { it.bounds.bottom }?.coerceAtLeast(1) ?: 1 + return snapshot.nodes.filter { node -> + node.visible && node.enabled && node.scrollable && + node.bounds.width * 100 >= screenWidth * 60 && + node.bounds.height * 100 >= screenHeight * 20 && + node.bounds.left >= panel.bounds.left && node.bounds.right <= panel.bounds.right && + node.bounds.top >= panel.bounds.top && node.bounds.bottom <= panel.bounds.bottom + }.distinctBy { it.path } + } + private fun purchasePanelScrollTargets(snapshot: UiSnapshot): List { val screenWidth = snapshot.nodes.maxOfOrNull { it.bounds.right }?.coerceAtLeast(1) ?: 1 val screenHeight = snapshot.nodes.maxOfOrNull { it.bounds.bottom }?.coerceAtLeast(1) ?: 1 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 5041a11..e3e2a29 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 @@ -141,6 +141,44 @@ class PurchaseLiveAutomationTest { assertEquals(0, driver.submitClicks) } + @Test + fun `saved address back into short spec panel restores without aspect ratio restriction`() { + val driver = LiveDriver(savedTransitionWithoutLegacyContext = true, backReturnsToSpecPanel = true, + savedPanelBounds = NodeBounds(0, 400, 1080, 1325)) + val automation = PurchaseLiveAutomation(driver, pause = {}) + val address = automation.updateShippingAddress("_cg81") + + assertEquals("_cg81", automation.finalConfirmation(input().copy(addressSuffix = "_cg81"), address).addressSuffix) + assertEquals(1, driver.backCount) + assertEquals(1, driver.scopedSwipes) + assertEquals(0, driver.submitClicks) + } + + @Test + fun `short saved spec panel still stops when unchanged`() { + val driver = LiveDriver(saveReturnsToSpecPanel = true, savedSpecPanelRecoveryStuck = true, + savedPanelBounds = NodeBounds(0, 400, 1080, 1325)) + val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg81") } + .exceptionOrNull() as PurchaseLiveException + assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code) + assertEquals(0, driver.backCount) + assertTrue(driver.scopedSwipes in 1..5) + assertEquals(0, driver.submitClicks) + } + + @Test + fun `missing and duplicate saved panel scroll regions fail distinctly without gestures`() { + for (count in listOf(0, 2)) { + val driver = LiveDriver(saveReturnsToSpecPanel = true, savedPanelScrollCount = count) + val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg81") } + .exceptionOrNull() as PurchaseLiveException + assertEquals("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", error.code) + assertTrue(error.message.orEmpty().contains(if (count == 0) "未找到可用" else "找到多个")) + assertEquals(0, driver.scopedSwipes) + assertEquals(0, driver.submitClicks) + } + } + @Test fun `saved address back into spec panel restores final evidence with one back`() { val driver = LiveDriver(savedTransitionWithoutLegacyContext = true, backReturnsToSpecPanel = true) @@ -674,6 +712,8 @@ class PurchaseLiveAutomationTest { private val savedTransitionHidesSuffix: Boolean = false, private val saveReturnsToSpecPanel: Boolean = false, private val backReturnsToSpecPanel: Boolean = false, + private val savedPanelBounds: NodeBounds = NodeBounds(0, 400, 1080, 2100), + private val savedPanelScrollCount: Int = 1, private val savedSpecPanelRecoveryStuck: Boolean = false, private val duplicatePhoneNodesSameCard: Boolean = false, private val duplicateSemanticAddressCards: Boolean = false, @@ -737,7 +777,7 @@ class PurchaseLiveAutomationTest { )) "post-save-spec" -> snapshot(listOf( node("root", "", bounds = NodeBounds(0, 0, 1080, 2200)), - node("panel", "", scrollable = true, bounds = NodeBounds(0, 400, 1080, 2100)), + node("panel", "", scrollable = true, bounds = savedPanelBounds), node("panel-title", "确认款式", bounds = NodeBounds(20, 396, 300, 430)), node("panel/price", "¥20.00", parentPath = "panel", bounds = NodeBounds(20, 460, 300, 520)), node("panel/selected", "已选 黑色 XL", parentPath = "panel", bounds = NodeBounds(20, 540, 700, 600)), @@ -749,7 +789,8 @@ class PurchaseLiveAutomationTest { node("panel/confirm", "确定", clickable = true, parentPath = "panel", bounds = NodeBounds(20, 1200, 500, 1280)), node("submit-parent", "", clickable = true, bounds = NodeBounds(20, 1900, 1000, 2100)), node("submit", "提交订单", parentPath = "submit-parent", bounds = NodeBounds(520, 1940, 980, 2040)), - )) + ).map { if (it.path == "panel" && savedPanelScrollCount == 0) it.copy(enabled = false) else it } + + if (savedPanelScrollCount > 1) listOf(node("panel/duplicate", "", scrollable = true, bounds = savedPanelBounds, parentPath = "panel")) else emptyList()) "order" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("time", "下单时间:2026-08-21 10:30:00"), node("pay", "立即支付", clickable = true))) "order-folded" -> snapshot(listOf(node("status", "待付款"), node("pay", "立即支付", clickable = true))) "order-folded-payment-activity" -> UiSnapshot(PDD, "com.xunmeng.pinduoduo.app_pay.core.PayActivity", listOf(