fix(#150): verify PDD entry after browser click
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 33
|
||||
versionName = "0.9.20"
|
||||
versionCode = 34
|
||||
versionName = "0.9.21"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+32
-7
@@ -6,6 +6,10 @@ import java.net.URLDecoder
|
||||
interface PurchaseUiDriver {
|
||||
fun capture(): UiSnapshot
|
||||
fun clickFresh(target: SnapshotNode): FreshActionResult
|
||||
fun clickFreshDetailed(target: SnapshotNode): FreshClickOutcome = FreshClickOutcome(
|
||||
result = clickFresh(target),
|
||||
reason = FreshClickReason.UNKNOWN,
|
||||
)
|
||||
fun tapPurchaseFresh(target: SnapshotNode): FreshActionResult
|
||||
fun inputFresh(target: SnapshotNode, value: String): FreshActionResult
|
||||
fun swipePurchase(direction: SwipeDirection, durationMs: Long): Boolean
|
||||
@@ -156,20 +160,38 @@ class PurchaseRehearsalExecutor(
|
||||
private fun openProduct(input: PurchaseExecutionInput, action: PurchaseAction): PurchaseExecutionOutcome? {
|
||||
if (!openLink(input.url)) return failure("PDD_LINK_INVALID", "任务中的 PDD 链接无法打开")
|
||||
val aliases = action.textAliases ?: listOf("打开拼多多APP", "打开拼多多 App", "打开")
|
||||
repeat(50) {
|
||||
var clickAttempted = false
|
||||
var nextClickPoll = 0
|
||||
var lastClickReason = FreshClickReason.UNKNOWN
|
||||
repeat(OPEN_PRODUCT_POLL_LIMIT) { poll ->
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)?.let { return it }
|
||||
if (snapshot.packageName == PDD_PACKAGE) return null
|
||||
val candidates = snapshot.nodes.filter { it.visible && it.enabled && it.label in aliases }
|
||||
if (candidates.size > 1) return failure("RULE_AMBIGUOUS", "打开拼多多按钮不唯一")
|
||||
if (candidates.size == 1) {
|
||||
return when (driver.clickFresh(candidates.single())) {
|
||||
FreshActionResult.SUCCESS -> null
|
||||
FreshActionResult.AMBIGUOUS -> failure("RULE_AMBIGUOUS", "打开拼多多按钮不唯一")
|
||||
else -> failure("RULE_ACTION_FAILED", "打开拼多多失败")
|
||||
if (candidates.size == 1 && poll >= nextClickPoll) {
|
||||
clickAttempted = true
|
||||
val outcome = driver.clickFreshDetailed(candidates.single())
|
||||
lastClickReason = outcome.reason
|
||||
if (outcome.result == FreshActionResult.AMBIGUOUS) {
|
||||
return failure("RULE_AMBIGUOUS", "打开拼多多按钮不唯一")
|
||||
}
|
||||
// The browser can rerender while handing the URL to PDD, and
|
||||
// ACTION_CLICK may report false after the app transition has
|
||||
// already started. The observed PDD foreground is the success
|
||||
// condition; retry only this unique target after a short bound.
|
||||
nextClickPoll = poll + OPEN_PRODUCT_RETRY_POLLS
|
||||
}
|
||||
pause(100)
|
||||
pause(OPEN_PRODUCT_POLL_MILLIS)
|
||||
}
|
||||
if (clickAttempted) {
|
||||
val message = when (lastClickReason) {
|
||||
FreshClickReason.ROOT_UNAVAILABLE, FreshClickReason.TARGET_NOT_FOUND -> "打开拼多多入口发生变化"
|
||||
FreshClickReason.NO_CLICKABLE_ANCESTOR -> "打开拼多多入口暂时无法点击"
|
||||
FreshClickReason.ACTION_CLICK_FALSE -> "打开拼多多后未进入商品页面"
|
||||
else -> "打开拼多多失败"
|
||||
}
|
||||
return failure("RULE_ACTION_FAILED", message)
|
||||
}
|
||||
return failure("PDD_DETAIL_ENTRY_FAILED", "没有进入拼多多商品页面")
|
||||
}
|
||||
@@ -379,6 +401,9 @@ class PurchaseRehearsalExecutor(
|
||||
|
||||
companion object {
|
||||
private const val PDD_PACKAGE = "com.xunmeng.pinduoduo"
|
||||
private const val OPEN_PRODUCT_POLL_LIMIT = 50
|
||||
private const val OPEN_PRODUCT_RETRY_POLLS = 10
|
||||
private const val OPEN_PRODUCT_POLL_MILLIS = 100L
|
||||
val DEFAULT_COLLECTOR = PddCollectorConfig(
|
||||
collectorId = "pddProductDetailV1",
|
||||
specEntryStrategy = "safeBottomSpecEntryV1",
|
||||
|
||||
@@ -96,6 +96,47 @@ class PurchaseRehearsalExecutorTest {
|
||||
assertTrue(driver.clicked.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed click result still succeeds when pdd actually opens`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
openClickResults = mutableListOf(FreshActionResult.FAILED),
|
||||
openPddOnFailedClick = 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.openClickCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `browser rerender retries the same unique open target within the bound`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
openClickResults = mutableListOf(FreshActionResult.NOT_FOUND, FreshActionResult.SUCCESS),
|
||||
)
|
||||
val pauses = mutableListOf<Long>()
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = pauses::add)
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertEquals(2, driver.openClickCount)
|
||||
assertTrue(pauses.size <= 50)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `persistent open click failure is bounded and remains safely failed`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
openClickResults = MutableList(10) { FreshActionResult.FAILED },
|
||||
)
|
||||
val pauses = mutableListOf<Long>()
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = pauses::add)
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("RULE_ACTION_FAILED", outcome.errorCode)
|
||||
assertTrue(driver.openClickCount in 1..5)
|
||||
assertEquals(50, pauses.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `goods id mismatch fails before opening pdd`() {
|
||||
var opened = false
|
||||
@@ -223,6 +264,8 @@ class PurchaseRehearsalExecutorTest {
|
||||
private val bottomPurchaseEntry: Boolean = false,
|
||||
private val includeReviewEntry: Boolean = false,
|
||||
private val hiddenSizeUntilUpSwipes: Int = 0,
|
||||
private val openClickResults: MutableList<FreshActionResult> = mutableListOf(),
|
||||
private val openPddOnFailedClick: Boolean = false,
|
||||
) : PurchaseUiDriver {
|
||||
var browser = false
|
||||
var panel = false
|
||||
@@ -231,6 +274,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
var quantity = 1L
|
||||
var swipeCount = 0
|
||||
var upSwipeCount = 0
|
||||
var openClickCount = 0
|
||||
val clicked = mutableListOf<String>()
|
||||
|
||||
override fun capture(): UiSnapshot {
|
||||
@@ -278,7 +322,12 @@ class PurchaseRehearsalExecutorTest {
|
||||
override fun clickFresh(target: SnapshotNode): FreshActionResult {
|
||||
clicked += target.label
|
||||
when (target.label) {
|
||||
"打开" -> browser = false
|
||||
"打开" -> {
|
||||
openClickCount++
|
||||
val result = openClickResults.removeFirstOrNull() ?: FreshActionResult.SUCCESS
|
||||
if (result == FreshActionResult.SUCCESS || openPddOnFailedClick) browser = false
|
||||
return result
|
||||
}
|
||||
"选择规格", "免拼购买" -> panel = true
|
||||
"XL" -> size = "XL"
|
||||
in colors -> color = target.label
|
||||
|
||||
Reference in New Issue
Block a user