fix(android): await saved panel confirmation before scrolling #261
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
+40
-4
@@ -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},未创建订单")
|
||||
}
|
||||
|
||||
+3
-2
@@ -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("复购价,", "专享优惠,")) {
|
||||
|
||||
+11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user