fix(android): recognize clipped address cards (#193)

This commit is contained in:
QiuSW
2026-09-01 18:07:09 +08:00
parent bb879be836
commit a1a9343bf6
2 changed files with 33 additions and 20 deletions
@@ -314,22 +314,26 @@ class PurchaseLiveAutomation(
}
private fun addressEntryTargets(snapshot: UiSnapshot, panelBounds: NodeBounds): List<SnapshotNode> {
val candidates = snapshot.nodes.filter { node ->
node.visible && node.enabled && MASKED_PHONE.containsMatchIn(node.label) && fullyInside(node.bounds, panelBounds)
}.distinctBy { it.path }
if (candidates.size <= 1) return candidates
val byPath = snapshot.nodes.associateBy { it.path }
val cards = candidates.map { source ->
var ancestor = source.parentPath?.let(byPath::get)
while (ancestor != null && !ancestor.clickable) ancestor = ancestor.parentPath?.let(byPath::get)
ancestor?.path?.let { it to source }
}
if (cards.all { it != null }) {
return cards.filterNotNull().groupBy({ it.first }, { it.second }).values.map { sources ->
sources.minBy { it.bounds.width.toLong() * it.bounds.height }
val cards = snapshot.nodes.filter { node ->
node.visible && node.enabled && MASKED_PHONE.containsMatchIn(node.label)
}.mapNotNull { source ->
var target: SnapshotNode? = source
while (target != null && !target.clickable) target = target.parentPath?.let(byPath::get)
target?.takeIf { card ->
card.visible && card.enabled && visiblyIntersects(card.bounds, panelBounds)
}
}
return mergedDirect(candidates)
}.distinctBy { it.path }
return mergedDirect(cards)
}
private fun visiblyIntersects(target: NodeBounds, scope: NodeBounds): Boolean {
val overlapWidth = (minOf(target.right, scope.right) - maxOf(target.left, scope.left)).coerceAtLeast(0)
val overlapHeight = (minOf(target.bottom, scope.bottom) - maxOf(target.top, scope.top)).coerceAtLeast(0)
val comparableWidth = minOf(target.width, scope.width)
return comparableWidth > 0 && target.height > 0 &&
overlapWidth * 100 >= comparableWidth * 60 &&
overlapHeight >= minOf(ADDRESS_CARD_MIN_VISIBLE_HEIGHT, target.height)
}
private fun verifiedPurchaseSurfaceBounds(snapshot: UiSnapshot, input: PurchaseExecutionInput): NodeBounds? {
@@ -372,9 +376,6 @@ class PurchaseLiveAutomation(
private fun inside(child: NodeBounds, parent: NodeBounds): Boolean =
child.centerX in parent.left..parent.right && child.centerY in parent.top..parent.bottom
private fun fullyInside(child: NodeBounds, parent: NodeBounds): Boolean =
child.left >= parent.left && child.top >= parent.top && child.right <= parent.right && child.bottom <= parent.bottom
private fun uniqueClickable(snapshot: UiSnapshot, nodes: List<SnapshotNode>): List<SnapshotNode> {
val byPath = snapshot.nodes.associateBy { it.path }
return nodes.mapNotNull { source ->
@@ -494,6 +495,7 @@ class PurchaseLiveAutomation(
const val PDD_PACKAGE = "com.xunmeng.pinduoduo"
const val WECHAT_PACKAGE = "com.tencent.mm"
val MASKED_PHONE = Regex("(?<![0-9])[0-9]{3}\\*{4}[0-9]{4}(?![0-9])")
const val ADDRESS_CARD_MIN_VISIBLE_HEIGHT = 24
val FINAL_SUBMIT_MARKERS = listOf("提交订单", "现在买,仅", "确认购买")
val PAYMENT_MARKERS = listOf("立即支付", "确认支付", "输入支付密码")
val UNPAID_MARKERS = listOf("待付款", "待支付", "去支付")
@@ -325,13 +325,14 @@ class PurchaseLiveAutomationTest {
@Test
fun `verified purchase surface pulls down when PDD exposes no scrollable panel`() {
val driver = LiveDriver(addressClipped = true, panelInitiallyScrollable = false)
val driver = LiveDriver(addressClipped = true, panelInitiallyScrollable = false, addressAtTopAfterReveal = true)
val address = PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress(input().copy(addressSuffix = "_cg56"))
assertEquals("广东省广州市天园街道骏景花园骏晖轩1202_cg56", address.expectedAddress)
assertEquals(0, driver.scopedSwipes)
assertEquals(1, driver.surfacePullDowns)
assertEquals(1, driver.addressTaps)
assertEquals("address-card", driver.lastAddressTapPath)
}
@Test
@@ -412,6 +413,7 @@ class PurchaseLiveAutomationTest {
private val panelInitiallyScrollable: Boolean = true,
private val panelScrollableAfterReveal: Boolean = true,
private val hideSelectedSummary: Boolean = false,
private val addressAtTopAfterReveal: Boolean = false,
private val chooserAfterSubmit: Boolean = false,
private val trustedChooser: Boolean = true,
private val splitConfirmationAddress: Boolean = false,
@@ -436,6 +438,7 @@ class PurchaseLiveAutomationTest {
var genericSwipes = 0
var surfacePullDowns = 0
var addressTaps = 0
var lastAddressTapPath: String? = null
var lastInputTargetPath: String? = null
var backCount = 0
var postSubmitBackCount = 0
@@ -506,7 +509,14 @@ class PurchaseLiveAutomationTest {
nodes += node("phone", "138****5678", bounds = NodeBounds(20, 650, 300, 710), parentPath = "address-card")
nodes += node("phone-duplicate", "138****5678", bounds = NodeBounds(20, 735, 300, 795), parentPath = "address-card")
}
else -> nodes += node("phone", "138****5678", bounds = NodeBounds(20, 650, 300, 710))
addressAtTopAfterReveal -> {
nodes += node("address-card", "", clickable = true, bounds = NodeBounds(0, 300, 1080, 520))
nodes += node("phone", "138****5678", bounds = NodeBounds(300, 330, 800, 380), parentPath = "address-card")
}
else -> {
nodes += node("address-card", "", clickable = true, bounds = NodeBounds(0, 620, 1080, 850))
nodes += node("phone", "138****5678", bounds = NodeBounds(20, 650, 300, 710), parentPath = "address-card")
}
}
val suffixStart = address.lastIndexOf("_cg")
val addressBody = if (suffixStart >= 0) address.substring(0, suffixStart) else address
@@ -550,8 +560,9 @@ class PurchaseLiveAutomationTest {
override fun tapPurchaseFresh(target: SnapshotNode): FreshActionResult {
addressTaps++
lastAddressTapPath = target.path
clicked += target.label
if (target.label == "138****5678") page = "panel"
if (target.path.startsWith("address-card")) page = "panel"
return FreshActionResult.SUCCESS
}