From d0a82f3e0181ca46bbd1a2c0255a8d281b5951af Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 10 Sep 2026 16:56:23 +0800 Subject: [PATCH] fix(android): await saved panel confirmation before scrolling #261 --- android/app/build.gradle.kts | 4 +- .../automation/PurchaseLiveAutomation.kt | 44 +++++++++++++++++-- .../persistence/PurchaseDiagnosticStore.kt | 5 ++- .../agent/PurchaseLiveAutomationTest.kt | 25 ++++++++++- .../PurchaseDiagnosticStoreTest.kt | 11 +++++ 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c3b53ba..b7d19bb 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 = 87 - versionName = "0.9.74" + versionCode = 88 + versionName = "0.9.75" 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 51259d5..d48d421 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 @@ -506,18 +506,20 @@ class PurchaseLiveAutomation( val save = uniqueClickable(stable, stable.nodes.filter { it.visible && it.enabled && it.label == "保存" }) if (save.size != 1) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存按钮不唯一,未创建订单") click(save.single(), "保存地址", 1_000) - var savedEvidence = waitForStableAddressEditorExit() + var savedEvidence = waitForSavedConfirmation(waitForStableAddressEditorExit(), expected, suffix) if (!savedAddressPageReady(savedEvidence, expected, suffix)) { if (isPurchaseConfirmationPanel(savedEvidence)) { savedEvidence = restoreFinalEvidenceInCurrentPanel(savedEvidence, expected, suffix) } else { if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单") + pause(1_000) val returned = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { savedAddressPageReady(it, expected, suffix) || isPurchaseConfirmationPanel(it) } - savedEvidence = if (!savedAddressPageReady(returned, expected, suffix)) { - restoreFinalEvidenceInCurrentPanel(returned, expected, suffix) - } else returned + val settled = waitForSavedConfirmation(returned, expected, suffix) + savedEvidence = if (!savedAddressPageReady(settled, expected, suffix)) { + restoreFinalEvidenceInCurrentPanel(settled, expected, suffix) + } else settled } } val proof = ShippingAddressProof(expected, suffix) @@ -539,6 +541,39 @@ class PurchaseLiveAutomation( ) } + /** Wait for late address/footer nodes before deciding to scroll. No device actions here. */ + private fun waitForSavedConfirmation(initial: UiSnapshot, expected: String, suffix: String): UiSnapshot { + var snapshot = initial + var readyActivity: String? = null + repeat(50) { poll -> + pageProblem(snapshot) + // A stable address list needs the existing single Back, not a footer wait. + if (snapshot.nodes.any { it.visible && it.label == "收货地址" } && + addressModifyTargets(snapshot).size == 1) return snapshot + val ready = savedAddressPageReady(snapshot, expected, suffix) + if (ready && readyActivity != null && readyActivity == snapshot.activityName) { + savedConfirmationDiagnostic(snapshot, expected, suffix, poll + 1) + return snapshot + } + readyActivity = if (ready) snapshot.activityName else null + if (poll < 49) { + pause(200) + snapshot = driver.capture() + } + } + savedConfirmationDiagnostic(snapshot, expected, suffix, 50) + return snapshot + } + + private fun savedConfirmationDiagnostic(snapshot: UiSnapshot, expected: String, suffix: String, polls: Int) { + val address = resolveAddressEntries(snapshot) + diagnostic("savedAddressMatched=${hasSavedAddressEvidence(snapshot, expected, suffix)};" + + "savedPanelMatched=${savedPurchasePanel(snapshot, expected, suffix) != null};" + + "savedLegacySubmitCandidates=${legacySubmitTargets(snapshot).size};" + + "savedAddressCards=${address.addressCardCount};savedAddressTargets=${address.tapTargetCount};" + + "savedScrollCandidates=${savedAddressPanelScrollTargets(snapshot).size};savedConfirmationPolls=$polls") + } + private fun restoreFinalEvidenceInCurrentPanel( initial: UiSnapshot, expected: String, @@ -553,6 +588,7 @@ class PurchaseLiveAutomation( } val panels = savedAddressPanelScrollTargets(snapshot) if (panels.size != 1) { + savedConfirmationDiagnostic(snapshot, expected, suffix, 0) val reason = if (panels.isEmpty()) "未找到可用的规格面板滚动区域" else "找到多个规格面板滚动区域" fail("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", "地址保存后${reason},未创建订单") } diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStore.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStore.kt index 226e87a..6a181df 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStore.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStore.kt @@ -59,9 +59,10 @@ class PurchaseDiagnosticStore(private val directory: File, private val now: () - private val FILE_NAME = Regex("[1-9][0-9]*_[A-Za-z0-9-]{1,80}\\.jsonl") private val numbers = setOf("specEntryCandidates", "explicit", "nested", "bottomPurchase", "entryReadyWaitPolls", "entryReadyWaitMillis", "scrollables", "headings", "options", "entryWaitPolls", "entryWaitMillis", - "addressEditorCandidates", "addressEditorPolls") + "addressEditorCandidates", "addressEditorPolls", "savedLegacySubmitCandidates", "savedAddressCards", + "savedAddressTargets", "savedScrollCandidates", "savedConfirmationPolls") private val booleans = setOf("panelAlreadyOpen", "reviewPage", "pageEvidence", "pageChanged", "summary", "quantity", - "orderAction", "closeControl", "paymentArea") + "orderAction", "closeControl", "paymentArea", "savedAddressMatched", "savedPanelMatched") private val enums = mapOf( "event" to setOf("started"), "entrySource" to setOf("unknown", "explicit_selection", "nested_selection", "bottom_purchase", "bottom_purchase_rightmost"), 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 5550b33..f386505 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 @@ -33,7 +33,7 @@ class PurchaseLiveAutomationTest { PurchaseLiveAutomation(driver, pause = { pauses += it }).updateShippingAddress("_cg81") assertTrue(polls >= 4) assertEquals("editor-address", driver.lastInputTargetPath) - assertEquals(3, pauses.count { it == 1_000L }) + assertEquals(4, pauses.count { it == 1_000L }) assertEquals(0, driver.submitClicks) } @@ -47,6 +47,29 @@ class PurchaseLiveAutomationTest { assertFalse(driver.clicked.contains("保存地址")) } + @Test + fun `non scrollable saved panel waits for late address nodes without back or swipe`() { + val driver = LiveDriver(savedStructuredFooter = "复购价,") + var polls = 0 + driver.structuredTransform = { s -> + polls++ + s.copy(nodes = s.nodes.filterNot { + it.path.startsWith("root/popup/specs") || + (polls < 8 && it.path.startsWith("root/popup/address")) + }) + } + val logs = mutableListOf() + val automation = PurchaseLiveAutomation(driver, pause = {}, diagnostic = logs::add) + val proof = automation.updateShippingAddress("_cg81") + automation.finalConfirmation(input().copy(addressSuffix = "_cg81"), proof) + assertTrue(polls >= 9) + assertEquals(0, driver.scopedSwipes) + assertEquals(0, driver.backCount) + assertEquals(0, driver.submitClicks) + assertTrue(logs.any { it.contains("savedAddressMatched=true") && it.contains("savedScrollCandidates=0") }) + assertFalse(logs.any { it.contains("_cg81") || it.contains("138") }) + } + @Test fun `saved address recognizes split dynamic footer outside spec scroller without a swipe`() { for (label in listOf("复购价,", "专享优惠,")) { diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStoreTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStoreTest.kt index 1768cb8..5a84f0b 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStoreTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/PurchaseDiagnosticStoreTest.kt @@ -42,6 +42,17 @@ class PurchaseDiagnosticStoreTest { assertEquals("close_control", entry.getJSONObject("evidence").getString("reason")) } + @Test fun `saved confirmation diagnostics retain only bounded scalars`() { + val fields = PurchaseDiagnosticStore.sanitize("savedAddressMatched=false;savedPanelMatched=true;" + + "savedLegacySubmitCandidates=0;savedAddressCards=1;savedAddressTargets=1;" + + "savedScrollCandidates=0;savedConfirmationPolls=50;savedAddress=secret;" + + "savedAddressMatched=secret;savedScrollCandidates=-1") + assertEquals(7, fields.length()) + assertFalse(fields.getBoolean("savedAddressMatched")) + assertEquals(50, fields.getInt("savedConfirmationPolls")) + assertFalse(fields.toString().contains("secret")) + } + @Test fun `retains last five attempts and last 128 events including failure`() { val dir = temporary.newFolder() var time = 1000L