From b829a203dc3ceb24d15b7b18b21ffa083d9a492d Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Sat, 5 Sep 2026 17:30:40 +0800 Subject: [PATCH] fix(agent): retain purchase panel after address save (#227) --- android/app/build.gradle.kts | 4 +- .../automation/PurchaseLiveAutomation.kt | 58 +++++++++++++++++-- .../agent/PurchaseLiveAutomationTest.kt | 51 +++++++++++++++- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a539c89..af692ba 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 = 61 - versionName = "0.9.48" + versionCode = 62 + versionName = "0.9.49" 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 05ac1e3..a94ec5d 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 @@ -302,14 +302,62 @@ class PurchaseLiveAutomation( click(save.single(), "保存地址") val savedEvidence = waitForStableAddressEditorExit() if (!hasFinalSavedAddressEvidence(savedEvidence, expected, suffix)) { - if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单") - waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { - hasFinalSavedAddressEvidence(it, expected, suffix) + if (isPurchaseConfirmationPanel(savedEvidence)) { + restoreFinalEvidenceInCurrentPanel(savedEvidence, expected, suffix) + } else { + if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单") + waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { + hasFinalSavedAddressEvidence(it, expected, suffix) + } } } return ShippingAddressProof(expected, suffix) } + private fun isPurchaseConfirmationPanel(snapshot: UiSnapshot): Boolean { + if (snapshot.packageName != PDD_PACKAGE || shippingAddressEditors(snapshot).isNotEmpty()) return false + val screen = PddScreenParser.parse(snapshot, PurchaseRehearsalExecutor.DEFAULT_COLLECTOR, "", null) + return screen.specPanelType in setOf( + SpecPanelType.NORMAL_SCROLLABLE, + SpecPanelType.NON_SCROLLABLE_CONFIRMATION, + SpecPanelType.ORDER_CONFIRMATION, + ) + } + + private fun restoreFinalEvidenceInCurrentPanel( + initial: UiSnapshot, + expected: String, + suffix: String, + ) { + var snapshot = initial + var previousSignature: String? = null + repeat(ADDRESS_CONFIRMATION_SCROLL_LIMIT) { attempt -> + if (hasFinalSavedAddressEvidence(snapshot, expected, suffix)) return + if (!isPurchaseConfirmationPanel(snapshot)) { + fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后采购面板发生变化,未创建订单") + } + val panels = purchasePanelScrollTargets(snapshot) + if (panels.size != 1) { + fail("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", "地址保存后没有找到唯一的规格面板滚动区域,未创建订单") + } + val panel = panels.single() + val signature = viewportSignature(snapshot, panel) + if (signature == previousSignature) { + fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后规格面板未找到完整订单确认信息,未创建订单") + } + previousSignature = signature + if (!driver.swipePurchaseIn(panel, SwipeDirection.DOWN, 350)) { + fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后规格面板无法定位订单确认信息,未创建订单") + } + if (attempt < ADDRESS_CONFIRMATION_SCROLL_LIMIT - 1) pause(ADDRESS_CONFIRMATION_SCROLL_INTERVAL_MS) + snapshot = driver.capture() + pageProblem(snapshot) + } + if (!hasFinalSavedAddressEvidence(snapshot, expected, suffix)) { + fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后规格面板未找到完整订单确认信息,未创建订单") + } + } + private fun hasSavedAddressEvidence(snapshot: UiSnapshot, expected: String, suffix: String): Boolean { if (snapshot.packageName != PDD_PACKAGE || shippingAddressEditors(snapshot).isNotEmpty()) return false val visible = snapshot.nodes.filter { it.visible } @@ -372,7 +420,7 @@ class PurchaseLiveAutomation( private fun viewportSignature(snapshot: UiSnapshot, panel: SnapshotNode): String = snapshot.nodes .filter { node -> node.visible && inside(node.bounds, panel.bounds) } .joinToString("|") { node -> - listOf(node.className.orEmpty(), node.bounds.left, node.bounds.top, node.bounds.right, node.bounds.bottom, node.clickable, node.scrollable).joinToString(":") + listOf(node.path, node.className.orEmpty(), node.bounds.left, node.bounds.top, node.bounds.right, node.bounds.bottom, node.clickable, node.scrollable).joinToString(":") } private fun inside(child: NodeBounds, parent: NodeBounds): Boolean = @@ -535,5 +583,7 @@ class PurchaseLiveAutomation( const val ORDER_RESULT_SAMPLE_INTERVAL_MS = 200L const val SPEC_CONFIRMATION_MAX_SAMPLES = 20 const val SPEC_CONFIRMATION_SAMPLE_INTERVAL_MS = 100L + const val ADDRESS_CONFIRMATION_SCROLL_LIMIT = 5 + const val ADDRESS_CONFIRMATION_SCROLL_INTERVAL_MS = 500L } } 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 890a9c9..0f210e8 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 @@ -104,6 +104,31 @@ class PurchaseLiveAutomationTest { assertEquals(0, driver.submitClicks) } + @Test + fun `saved address returning to spec panel is recovered without pressing back`() { + val driver = LiveDriver(saveReturnsToSpecPanel = true) + val automation = PurchaseLiveAutomation(driver, pause = {}) + + val address = automation.updateShippingAddress("_cg91") + val final = automation.finalConfirmation(input().copy(addressSuffix = "_cg91"), address) + + assertEquals("_cg91", final.addressSuffix) + assertEquals(0, driver.backCount) + assertEquals(1, driver.scopedSwipes) + assertEquals(0, driver.submitClicks) + } + + @Test + fun `saved address stuck on spec panel fails without backing out or submitting`() { + val driver = LiveDriver(saveReturnsToSpecPanel = true, savedSpecPanelRecoveryStuck = true) + val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg92") } + .exceptionOrNull() as PurchaseLiveException + + assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code) + assertEquals(0, driver.backCount) + assertEquals(0, driver.submitClicks) + } + @Test fun `edit field containing suffix cannot impersonate post save evidence`() { val driver = LiveDriver(saveStaysInEdit = true) @@ -526,6 +551,8 @@ class PurchaseLiveAutomationTest { private val saveStaysInEdit: Boolean = false, private val savedTransitionWithoutLegacyContext: Boolean = false, private val savedTransitionHidesSuffix: Boolean = false, + private val saveReturnsToSpecPanel: Boolean = false, + private val savedSpecPanelRecoveryStuck: Boolean = false, private val wechatLoginAfterSubmit: Boolean = false, private val wechatRestoreStuck: Boolean = false, private val orderEvidenceBelowFold: Boolean = false, @@ -581,6 +608,21 @@ class PurchaseLiveAutomationTest { node("transition-title", "选择收货信息"), node("address-summary", if (savedTransitionHidesSuffix) "已保存的收货信息" else address.substring(address.lastIndexOf("_cg"))), )) + "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-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)), + node("panel/color-heading", "颜色分类", parentPath = "panel", bounds = NodeBounds(20, 650, 300, 700)), + node("panel/color", "黑色", clickable = true, parentPath = "panel", bounds = NodeBounds(20, 720, 220, 790)), + node("panel/size-heading", "尺码", parentPath = "panel", bounds = NodeBounds(20, 850, 300, 900)), + node("panel/size", "XL", clickable = true, parentPath = "panel", bounds = NodeBounds(20, 920, 220, 990)), + node("panel/quantity", "2", className = "android.widget.EditText", parentPath = "panel", bounds = NodeBounds(400, 1050, 600, 1120)), + 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)), + )) "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( @@ -639,7 +681,13 @@ class PurchaseLiveAutomationTest { when (target.label) { "138****5678" -> page = "panel" "修改" -> page = "edit" - "保存" -> if (!saveStaysInEdit) page = if (savedTransitionWithoutLegacyContext) "saved-transition" else "panel" + "保存" -> if (!saveStaysInEdit) { + page = when { + saveReturnsToSpecPanel -> "post-save-spec" + savedTransitionWithoutLegacyContext -> "saved-transition" + else -> "panel" + } + } "提交订单" -> { submitClicks++ page = when { @@ -677,6 +725,7 @@ class PurchaseLiveAutomationTest { override fun swipePurchaseIn(target: SnapshotNode, direction: SwipeDirection, durationMs: Long): Boolean { scopedSwipes++ if (target.path == "panel" && direction == SwipeDirection.DOWN) addressVisible = true + if (page == "post-save-spec" && direction == SwipeDirection.DOWN && !savedSpecPanelRecoveryStuck) page = "confirmation" return true } override fun backPurchase(): Boolean {