fix(#64): bound purchase spec selection retries
This commit is contained in:
+47
-6
@@ -215,12 +215,9 @@ class PurchaseRehearsalExecutor(
|
||||
}
|
||||
listOf("color" to input.mappedColor, "size" to input.mappedSize).forEach { (dimension, target) ->
|
||||
if (target.isBlank()) return@forEach
|
||||
val screen = currentScreen(input)
|
||||
screen.problem?.let { return failure(it.code, it.message) }
|
||||
val values = screen.dimensions.filter { it.key == dimension }.flatMap { it.values }.filter { it.text == target && it.available }
|
||||
if (values.isEmpty()) return failure("PURCHASE_SPEC_NOT_MATCHED", "没有找到规格:$target")
|
||||
if (values.size != 1) return failure("RULE_AMBIGUOUS", "规格 $target 匹配到多个控件")
|
||||
when (driver.clickFresh(values.single().node)) {
|
||||
val lookup = locateExactSpec(input, dimension, target)
|
||||
lookup.failure?.let { return it }
|
||||
when (driver.clickFresh(requireNotNull(lookup.node))) {
|
||||
FreshActionResult.AMBIGUOUS -> return failure("RULE_AMBIGUOUS", "规格 $target 匹配到多个控件")
|
||||
FreshActionResult.SUCCESS -> Unit
|
||||
else -> return failure("RULE_ACTION_FAILED", "规格 $target 选择失败")
|
||||
@@ -244,6 +241,50 @@ class PurchaseRehearsalExecutor(
|
||||
return null
|
||||
}
|
||||
|
||||
private data class SpecLookup(val node: SnapshotNode? = null, val failure: PurchaseExecutionOutcome? = null)
|
||||
|
||||
/**
|
||||
* Searches only parsed, selectable values in the already-open spec panel.
|
||||
* A short downward pass first restores the top when a previous action left
|
||||
* the panel lower down; the bounded upward pass then visits lower values.
|
||||
* Every gesture is followed by a fresh parse, and unchanged evidence ends
|
||||
* that direction early.
|
||||
*/
|
||||
private fun locateExactSpec(input: PurchaseExecutionInput, dimension: String, target: String): SpecLookup {
|
||||
fun inspect(): Pair<SpecLookup?, String> {
|
||||
val screen = currentScreen(input)
|
||||
screen.problem?.let { return SpecLookup(failure = failure(it.code, it.message)) to "" }
|
||||
if (!screen.specPanelOpen) {
|
||||
return SpecLookup(failure = failure("RULE_NOT_MATCHED", "商品规格面板已经关闭")) to ""
|
||||
}
|
||||
val dimensionValues = screen.dimensions.filter { it.key == dimension }.flatMap { it.values }
|
||||
val exact = dimensionValues.filter { it.text == target && it.available }
|
||||
if (exact.size > 1) {
|
||||
return SpecLookup(failure = failure("RULE_AMBIGUOUS", "规格 $target 匹配到多个控件")) to ""
|
||||
}
|
||||
if (exact.size == 1) return SpecLookup(node = exact.single().node) to ""
|
||||
val signature = screen.dimensions.joinToString("|") { item ->
|
||||
"${item.key}:${item.values.joinToString(",") { value -> "${value.text}:${value.available}" }}"
|
||||
}
|
||||
return null to signature
|
||||
}
|
||||
|
||||
var inspected = inspect()
|
||||
inspected.first?.let { return it }
|
||||
var currentSignature = inspected.second
|
||||
for ((direction, limit) in listOf(SwipeDirection.DOWN to 3, SwipeDirection.UP to 6)) {
|
||||
for (attempt in 0 until limit) {
|
||||
if (!driver.swipePurchase(direction, 350)) break
|
||||
pause(300)
|
||||
inspected = inspect()
|
||||
inspected.first?.let { return it }
|
||||
if (inspected.second == currentSignature) break
|
||||
currentSignature = inspected.second
|
||||
}
|
||||
}
|
||||
return SpecLookup(failure = failure("PURCHASE_SPEC_NOT_MATCHED", "没有找到规格:$target"))
|
||||
}
|
||||
|
||||
private fun setQuantity(quantity: Long): PurchaseExecutionOutcome? {
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)?.let { return it }
|
||||
|
||||
@@ -63,6 +63,18 @@ class PurchaseRehearsalExecutorTest {
|
||||
assertEquals("spec_probe_completed", outcome.resultType)
|
||||
assertEquals("{\"dimensions\":[]}", outcome.probedSpecs)
|
||||
assertFalse(driver.clicked.contains("黑色加绒"))
|
||||
assertTrue(driver.swipeCount <= 11)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exact size below the visible panel is found after a bounded vertical search`() {
|
||||
val driver = FakePurchaseDriver(hiddenSizeUntilUpSwipes = 3)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertTrue(driver.clicked.contains("XL"))
|
||||
assertEquals(3, driver.upSwipeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -209,6 +221,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
private val duplicateOpen: Boolean = false,
|
||||
private val bottomPurchaseEntry: Boolean = false,
|
||||
private val includeReviewEntry: Boolean = false,
|
||||
private val hiddenSizeUntilUpSwipes: Int = 0,
|
||||
) : PurchaseUiDriver {
|
||||
var browser = false
|
||||
var panel = false
|
||||
@@ -216,6 +229,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
var size: String? = null
|
||||
var quantity = 1L
|
||||
var swipeCount = 0
|
||||
var upSwipeCount = 0
|
||||
val clicked = mutableListOf<String>()
|
||||
|
||||
override fun capture(): UiSnapshot {
|
||||
@@ -251,7 +265,8 @@ class PurchaseRehearsalExecutorTest {
|
||||
nodes += node("scroll/color-$index", value, 20 + index * 220, 470, 200 + index * 220, 540, clickable = true, selected = color == value, parentPath = "scroll")
|
||||
}
|
||||
nodes += node("scroll/size-heading", "尺码", 20, 650, 300, 690, parentPath = "scroll")
|
||||
nodes += node("scroll/size", "XL", 20, 710, 200, 780, clickable = true, selected = size == "XL", parentPath = "scroll")
|
||||
val visibleSize = if (upSwipeCount >= hiddenSizeUntilUpSwipes) "XL" else "S"
|
||||
nodes += node("scroll/size", visibleSize, 20, 710, 200, 780, clickable = true, selected = size == visibleSize, parentPath = "scroll")
|
||||
nodes += node("quantity", quantity.toString(), 400, 800, 600, 870, className = "android.widget.EditText")
|
||||
nodes += node("confirm", "确定", 20, 900, 500, 980, clickable = true)
|
||||
nodes += node("order", "提交订单", 20, 1100, 500, 1180, clickable = true)
|
||||
@@ -279,6 +294,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
|
||||
override fun swipePurchase(direction: SwipeDirection, durationMs: Long): Boolean {
|
||||
swipeCount++
|
||||
if (direction == SwipeDirection.UP) upSwipeCount++
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user