fix(android): wait for stable address editor with transition buffers #261
This commit is contained in:
+29
-8
@@ -32,6 +32,7 @@ class PurchaseLiveException(val code: String, message: String, val actualUnitPri
|
||||
class PurchaseLiveAutomation(
|
||||
private val driver: PurchaseUiDriver,
|
||||
private val pause: (Long) -> Unit = Thread::sleep,
|
||||
private val diagnostic: (String) -> Unit = {},
|
||||
) {
|
||||
private var submitAttempted = false
|
||||
private var savedAddressProof: ShippingAddressProof? = null
|
||||
@@ -126,7 +127,9 @@ class PurchaseLiveAutomation(
|
||||
}
|
||||
pause(1_000)
|
||||
snapshot = waitFor("PURCHASE_ADDRESS_PANEL_TIMEOUT", "收货地址页面打开超时,未创建订单") {
|
||||
it.nodes.any { node -> node.visible && node.label.replace(" ", "") == "收货地址" }
|
||||
it.packageName == PDD_PACKAGE &&
|
||||
it.nodes.any { node -> node.visible && node.label.replace(" ", "") == "收货地址" } &&
|
||||
addressModifyTargets(it).size == 1
|
||||
}
|
||||
return editAndVerifyAddress(snapshot, addressSuffix)
|
||||
}
|
||||
@@ -488,10 +491,8 @@ class PurchaseLiveAutomation(
|
||||
structuralSubmitAnchor = null
|
||||
val modify = addressModifyTargets(panel)
|
||||
if (modify.size != 1) fail("PURCHASE_ADDRESS_EDIT_AMBIGUOUS", "没有找到唯一的地址修改按钮,未创建订单")
|
||||
click(modify.single(), "修改地址")
|
||||
val edit = waitFor("PURCHASE_ADDRESS_EDIT_TIMEOUT", "地址编辑页面打开超时,未创建订单") { snapshot ->
|
||||
snapshot.nodes.any { it.visible && it.label.replace(" ", "").contains("详细地址") }
|
||||
}
|
||||
click(modify.single(), "修改地址", 1_000)
|
||||
val edit = waitForAddressEditorReady()
|
||||
val editors = shippingAddressEditors(edit)
|
||||
if (editors.size != 1) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "没有找到唯一的详细地址输入框,未创建订单")
|
||||
val current = editors.single().label.trim()
|
||||
@@ -504,7 +505,7 @@ 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(), "保存地址")
|
||||
click(save.single(), "保存地址", 1_000)
|
||||
var savedEvidence = waitForStableAddressEditorExit()
|
||||
if (!savedAddressPageReady(savedEvidence, expected, suffix)) {
|
||||
if (isPurchaseConfirmationPanel(savedEvidence)) {
|
||||
@@ -715,13 +716,33 @@ class PurchaseLiveAutomation(
|
||||
}.distinctBy { it.path }
|
||||
}
|
||||
|
||||
private fun click(node: SnapshotNode, label: String) {
|
||||
private fun waitForAddressEditorReady(): UiSnapshot {
|
||||
var previous: SnapshotNode? = null
|
||||
var previousActivity: String? = null
|
||||
var count = 0
|
||||
repeat(50) { poll ->
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)
|
||||
val editors = if (snapshot.packageName == PDD_PACKAGE) shippingAddressEditors(snapshot) else emptyList()
|
||||
count = editors.size
|
||||
diagnostic("addressEditorCandidates=$count;addressEditorPolls=${poll + 1}")
|
||||
val editor = editors.singleOrNull()
|
||||
if (editor != null && previous != null && snapshot.activityName == previousActivity &&
|
||||
editor.path == previous!!.path && editor.bounds == previous!!.bounds && editor.label == previous!!.label) return snapshot
|
||||
previous = editor
|
||||
previousActivity = snapshot.activityName
|
||||
pause(200)
|
||||
}
|
||||
fail("PURCHASE_ADDRESS_UPDATE_FAILED", "详细地址输入框未稳定就绪,未创建订单 [candidates=$count]")
|
||||
}
|
||||
|
||||
private fun click(node: SnapshotNode, label: String, settleMillis: Long = 500) {
|
||||
when (driver.clickFresh(node)) {
|
||||
FreshActionResult.SUCCESS -> Unit
|
||||
FreshActionResult.AMBIGUOUS -> fail("RULE_AMBIGUOUS", "$label 不唯一")
|
||||
else -> fail("RULE_ACTION_FAILED", "$label 点击失败")
|
||||
}
|
||||
pause(500)
|
||||
pause(settleMillis)
|
||||
}
|
||||
|
||||
private fun legacySubmitTargets(snapshot: UiSnapshot): List<SnapshotNode> = uniqueClickable(
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ class PurchaseRehearsalExecutor(
|
||||
var addressProof: ShippingAddressProof? = null
|
||||
var irreversibleStarted = false
|
||||
val specSelectionProofs = mutableMapOf<String, ExactSpecSelectionProof>()
|
||||
val live = PurchaseLiveAutomation(driver, pause)
|
||||
val live = PurchaseLiveAutomation(driver, pause, panelDiagnostic)
|
||||
for (action in rule.actions) {
|
||||
// The immediate phase-two handoff can reuse the PDD page retained by
|
||||
// spec_probe. A later manual retry may start from Agent (or another
|
||||
|
||||
+2
-1
@@ -58,7 +58,8 @@ class PurchaseDiagnosticStore(private val directory: File, private val now: () -
|
||||
private val VERSION = Regex("[0-9]+\\.[0-9]+\\.[0-9]+")
|
||||
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")
|
||||
"entryReadyWaitMillis", "scrollables", "headings", "options", "entryWaitPolls", "entryWaitMillis",
|
||||
"addressEditorCandidates", "addressEditorPolls")
|
||||
private val booleans = setOf("panelAlreadyOpen", "reviewPage", "pageEvidence", "pageChanged", "summary", "quantity",
|
||||
"orderAction", "closeControl", "paymentArea")
|
||||
private val enums = mapOf(
|
||||
|
||||
@@ -18,6 +18,35 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PurchaseLiveAutomationTest {
|
||||
@Test fun `address editor waits for loaded unique stable input after one second transitions`() {
|
||||
val driver = LiveDriver()
|
||||
var polls = 0
|
||||
driver.editTransform = { snapshot ->
|
||||
polls++
|
||||
when (polls) {
|
||||
1 -> snapshot.copy(nodes = snapshot.nodes.filterNot { it.path == "editor-address" })
|
||||
2 -> snapshot.copy(nodes = snapshot.nodes.map { if (it.path == "editor-address") it.copy(text = "") else it })
|
||||
else -> snapshot
|
||||
}
|
||||
}
|
||||
val pauses = mutableListOf<Long>()
|
||||
PurchaseLiveAutomation(driver, pause = { pauses += it }).updateShippingAddress("_cg81")
|
||||
assertTrue(polls >= 4)
|
||||
assertEquals("editor-address", driver.lastInputTargetPath)
|
||||
assertEquals(3, pauses.count { it == 1_000L })
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test fun `persistently ambiguous address editors never receive input`() {
|
||||
val driver = LiveDriver()
|
||||
driver.editTransform = { snapshot -> snapshot.copy(nodes = snapshot.nodes + snapshot.nodes.single { it.path == "editor-address" }.copy(path = "duplicate-editor")) }
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg81") }.exceptionOrNull() as PurchaseLiveException
|
||||
assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code)
|
||||
assertTrue(error.message!!.contains("candidates=2"))
|
||||
assertEquals(null, driver.lastInputTargetPath)
|
||||
assertFalse(driver.clicked.contains("保存地址"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saved address recognizes split dynamic footer outside spec scroller without a swipe`() {
|
||||
for (label in listOf("复购价,", "专享优惠,")) {
|
||||
@@ -902,13 +931,14 @@ class PurchaseLiveAutomationTest {
|
||||
var postSubmitCaptureCount = 0
|
||||
val currentPage: String get() = page
|
||||
var structuredTransform: (UiSnapshot) -> UiSnapshot = { it }
|
||||
var editTransform: (UiSnapshot) -> UiSnapshot = { it }
|
||||
|
||||
override fun capture(): UiSnapshot {
|
||||
if (submitClicks > 0) {
|
||||
postSubmitCaptureCount++
|
||||
if (pendingPostSubmitPages.isNotEmpty()) page = pendingPostSubmitPages.removeAt(0)
|
||||
}
|
||||
return currentSnapshot().let { if (page == "structured") structuredTransform(it) else it }
|
||||
return currentSnapshot().let { if (page == "structured") structuredTransform(it) else if (page == "edit") editTransform(it) else it }
|
||||
}
|
||||
|
||||
private fun currentSnapshot(): UiSnapshot = when (page) {
|
||||
|
||||
Reference in New Issue
Block a user