fix(android): await saved panel confirmation before scrolling #261

This commit is contained in:
QiuSW
2026-09-10 16:56:23 +08:00
parent 713111da0a
commit d0a82f3e01
5 changed files with 80 additions and 9 deletions
+2 -2
View File
@@ -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"
@@ -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},未创建订单")
}
@@ -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"),
@@ -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<String>()
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("复购价,", "专享优惠,")) {
@@ -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