fix(#151): verify exact spec selection after click
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 34
|
||||
versionName = "0.9.21"
|
||||
versionCode = 35
|
||||
versionName = "0.9.22"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+56
-18
@@ -240,24 +240,7 @@ class PurchaseRehearsalExecutor(
|
||||
}
|
||||
listOf("color" to input.mappedColor, "size" to input.mappedSize).forEach { (dimension, target) ->
|
||||
if (target.isBlank()) return@forEach
|
||||
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 选择失败")
|
||||
}
|
||||
var selected = false
|
||||
repeat(20) {
|
||||
val refreshed = currentScreen(input)
|
||||
refreshed.problem?.let { return failure(it.code, it.message) }
|
||||
selected = refreshed.selectedSummary?.contains(target) == true ||
|
||||
refreshed.dimensions.filter { it.key == dimension }.flatMap { it.values }
|
||||
.any { it.text == target && (it.node.selected || it.node.checked) }
|
||||
if (selected) return@repeat
|
||||
pause(100)
|
||||
}
|
||||
if (!selected) return failure("PURCHASE_SPEC_NOT_MATCHED", "规格 $target 未能精确选中")
|
||||
selectExactSpecWithRetry(input, dimension, target)?.let { return it }
|
||||
}
|
||||
val requiredCapability = if (input.executionMode == "live") PurchaseAgentCapabilities.LIVE_V1 else PurchaseAgentCapabilities.REHEARSAL_V1
|
||||
if (rule.requiredCapabilities.none { it == requiredCapability }) {
|
||||
@@ -266,6 +249,57 @@ class PurchaseRehearsalExecutor(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun selectExactSpecWithRetry(
|
||||
input: PurchaseExecutionInput,
|
||||
dimension: String,
|
||||
target: String,
|
||||
): PurchaseExecutionOutcome? {
|
||||
var lastClickReason = FreshClickReason.UNKNOWN
|
||||
var clickSucceeded = false
|
||||
repeat(SPEC_SELECTION_CLICK_ATTEMPTS) {
|
||||
val lookup = locateExactSpec(input, dimension, target)
|
||||
lookup.failure?.let { return it }
|
||||
val outcome = driver.clickFreshDetailed(requireNotNull(lookup.node))
|
||||
lastClickReason = outcome.reason
|
||||
if (outcome.result == FreshActionResult.AMBIGUOUS) {
|
||||
return failure("RULE_AMBIGUOUS", "规格 $target 匹配到多个控件")
|
||||
}
|
||||
clickSucceeded = clickSucceeded || outcome.result == FreshActionResult.SUCCESS
|
||||
|
||||
// PDD rerenders the remaining options after a color selection. A
|
||||
// stale node can report NOT_FOUND/FAILED even when the exact option
|
||||
// was selected. Always verify the observable selected state before
|
||||
// deciding whether to refind and retry the same exact target.
|
||||
val verifyPolls = if (outcome.result == FreshActionResult.SUCCESS) {
|
||||
SPEC_SELECTION_SUCCESS_VERIFY_POLLS
|
||||
} else {
|
||||
SPEC_SELECTION_FAILED_VERIFY_POLLS
|
||||
}
|
||||
repeat(verifyPolls) {
|
||||
val refreshed = currentScreen(input)
|
||||
refreshed.problem?.let { return failure(it.code, it.message) }
|
||||
if (isExactSpecSelected(refreshed, dimension, target)) return null
|
||||
pause(SPEC_SELECTION_POLL_MILLIS)
|
||||
}
|
||||
}
|
||||
|
||||
if (clickSucceeded) {
|
||||
return failure("PURCHASE_SPEC_NOT_MATCHED", "规格 $target 未能精确选中")
|
||||
}
|
||||
val message = when (lastClickReason) {
|
||||
FreshClickReason.ROOT_UNAVAILABLE, FreshClickReason.TARGET_NOT_FOUND -> "规格 $target 点击前发生变化"
|
||||
FreshClickReason.NO_CLICKABLE_ANCESTOR -> "规格 $target 暂时无法点击"
|
||||
FreshClickReason.ACTION_CLICK_FALSE -> "规格 $target 点击未生效"
|
||||
else -> "规格 $target 选择失败"
|
||||
}
|
||||
return failure("RULE_ACTION_FAILED", message)
|
||||
}
|
||||
|
||||
private fun isExactSpecSelected(screen: ParsedPddScreen, dimension: String, target: String): Boolean =
|
||||
screen.selectedSummary?.contains(target) == true ||
|
||||
screen.dimensions.filter { it.key == dimension }.flatMap { it.values }
|
||||
.any { it.text == target && (it.node.selected || it.node.checked) }
|
||||
|
||||
private data class SpecLookup(val node: SnapshotNode? = null, val failure: PurchaseExecutionOutcome? = null)
|
||||
|
||||
/**
|
||||
@@ -404,6 +438,10 @@ class PurchaseRehearsalExecutor(
|
||||
private const val OPEN_PRODUCT_POLL_LIMIT = 50
|
||||
private const val OPEN_PRODUCT_RETRY_POLLS = 10
|
||||
private const val OPEN_PRODUCT_POLL_MILLIS = 100L
|
||||
private const val SPEC_SELECTION_CLICK_ATTEMPTS = 3
|
||||
private const val SPEC_SELECTION_SUCCESS_VERIFY_POLLS = 20
|
||||
private const val SPEC_SELECTION_FAILED_VERIFY_POLLS = 5
|
||||
private const val SPEC_SELECTION_POLL_MILLIS = 100L
|
||||
val DEFAULT_COLLECTOR = PddCollectorConfig(
|
||||
collectorId = "pddProductDetailV1",
|
||||
specEntryStrategy = "safeBottomSpecEntryV1",
|
||||
|
||||
@@ -78,6 +78,46 @@ class PurchaseRehearsalExecutorTest {
|
||||
assertEquals(3, driver.upSwipeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed size click result continues when exact size is actually selected`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
sizeClickResults = mutableListOf(FreshActionResult.FAILED),
|
||||
sizeSelectsOnFailedClick = true,
|
||||
)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertEquals(1, driver.sizeClickCount)
|
||||
assertEquals("XL", driver.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rerendered exact size is refound and selected on the second attempt`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
sizeClickResults = mutableListOf(FreshActionResult.NOT_FOUND, FreshActionResult.SUCCESS),
|
||||
)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertEquals(2, driver.sizeClickCount)
|
||||
assertEquals("XL", driver.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `persistent exact size click failure stops after three attempts`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
sizeClickResults = MutableList(3) { FreshActionResult.FAILED },
|
||||
)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("RULE_ACTION_FAILED", outcome.errorCode)
|
||||
assertEquals(3, driver.sizeClickCount)
|
||||
assertEquals(null, driver.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price outside range fails before any order action`() {
|
||||
val driver = FakePurchaseDriver(priceCent = 4_000)
|
||||
@@ -266,6 +306,8 @@ class PurchaseRehearsalExecutorTest {
|
||||
private val hiddenSizeUntilUpSwipes: Int = 0,
|
||||
private val openClickResults: MutableList<FreshActionResult> = mutableListOf(),
|
||||
private val openPddOnFailedClick: Boolean = false,
|
||||
private val sizeClickResults: MutableList<FreshActionResult> = mutableListOf(),
|
||||
private val sizeSelectsOnFailedClick: Boolean = false,
|
||||
) : PurchaseUiDriver {
|
||||
var browser = false
|
||||
var panel = false
|
||||
@@ -275,6 +317,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
var swipeCount = 0
|
||||
var upSwipeCount = 0
|
||||
var openClickCount = 0
|
||||
var sizeClickCount = 0
|
||||
val clicked = mutableListOf<String>()
|
||||
|
||||
override fun capture(): UiSnapshot {
|
||||
@@ -329,7 +372,12 @@ class PurchaseRehearsalExecutorTest {
|
||||
return result
|
||||
}
|
||||
"选择规格", "免拼购买" -> panel = true
|
||||
"XL" -> size = "XL"
|
||||
"XL" -> {
|
||||
sizeClickCount++
|
||||
val result = sizeClickResults.removeFirstOrNull() ?: FreshActionResult.SUCCESS
|
||||
if (result == FreshActionResult.SUCCESS || sizeSelectsOnFailedClick) size = "XL"
|
||||
return result
|
||||
}
|
||||
in colors -> color = target.label
|
||||
"增加数量" -> quantity++
|
||||
"减少数量" -> quantity--
|
||||
|
||||
Reference in New Issue
Block a user