fix(android): wait for stable PDD product page (#222)
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 56
|
||||
versionName = "0.9.43"
|
||||
versionCode = 57
|
||||
versionName = "0.9.44"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+36
-6
@@ -213,10 +213,20 @@ class PurchaseRehearsalExecutor(
|
||||
var clickAttempted = false
|
||||
var nextClickPoll = 0
|
||||
var lastClickReason = FreshClickReason.UNKNOWN
|
||||
var stableEvidenceReads = 0
|
||||
var pddForegroundObserved = false
|
||||
repeat(OPEN_PRODUCT_POLL_LIMIT) { poll ->
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)?.let { return it }
|
||||
if (snapshot.packageName == PDD_PACKAGE) return null
|
||||
if (snapshot.packageName == PDD_PACKAGE) {
|
||||
pddForegroundObserved = true
|
||||
val screen = PddScreenParser.parse(snapshot, DEFAULT_COLLECTOR, input.goodsId, null)
|
||||
stableEvidenceReads = if (screen.hasPurchaseProductEvidence()) stableEvidenceReads + 1 else 0
|
||||
if (stableEvidenceReads >= PRODUCT_PAGE_STABLE_READS) return null
|
||||
pause(OPEN_PRODUCT_POLL_MILLIS)
|
||||
return@repeat
|
||||
}
|
||||
stableEvidenceReads = 0
|
||||
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 && poll >= nextClickPoll) {
|
||||
@@ -234,6 +244,9 @@ class PurchaseRehearsalExecutor(
|
||||
}
|
||||
pause(OPEN_PRODUCT_POLL_MILLIS)
|
||||
}
|
||||
if (pddForegroundObserved) {
|
||||
return failure("PDD_DETAIL_ENTRY_FAILED", "打开拼多多后未识别到稳定商品页面")
|
||||
}
|
||||
if (clickAttempted) {
|
||||
val message = when (lastClickReason) {
|
||||
FreshClickReason.ROOT_UNAVAILABLE, FreshClickReason.TARGET_NOT_FOUND -> "打开拼多多入口发生变化"
|
||||
@@ -247,14 +260,20 @@ class PurchaseRehearsalExecutor(
|
||||
}
|
||||
|
||||
private fun verifyProduct(input: PurchaseExecutionInput): PurchaseExecutionOutcome? {
|
||||
repeat(50) {
|
||||
var stableEvidenceReads = 0
|
||||
repeat(PRODUCT_PAGE_POLL_LIMIT) {
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)?.let { return it }
|
||||
val screen = PddScreenParser.parse(snapshot, DEFAULT_COLLECTOR, input.goodsId, null)
|
||||
if (screen.hasPurchaseProductEvidence()) {
|
||||
return recoverSoldOut(input, screen)
|
||||
stableEvidenceReads++
|
||||
if (stableEvidenceReads >= PRODUCT_PAGE_STABLE_READS) {
|
||||
return recoverSoldOut(input, screen)
|
||||
}
|
||||
} else {
|
||||
stableEvidenceReads = 0
|
||||
}
|
||||
pause(100)
|
||||
pause(OPEN_PRODUCT_POLL_MILLIS)
|
||||
}
|
||||
return failure("PDD_DETAIL_ENTRY_FAILED", "没有进入拼多多商品页面")
|
||||
}
|
||||
@@ -352,7 +371,10 @@ class PurchaseRehearsalExecutor(
|
||||
"规格入口手势目标不唯一 [${specEntryEvidence(screen, 1, entryReadyWaitPolls)}]",
|
||||
)
|
||||
FreshActionResult.SUCCESS -> Unit
|
||||
else -> return failure(SPEC_ENTRY_CLICK_FAILED, click.reason.specEntrySubreason())
|
||||
else -> return failure(
|
||||
SPEC_ENTRY_CLICK_FAILED,
|
||||
click.reason.specEntrySubreasonAfterGestureFailure(),
|
||||
)
|
||||
}
|
||||
wait = waitForSpecPanel(input, specActionSignature(wait.screen))
|
||||
wait.failure?.let { return it }
|
||||
@@ -459,6 +481,12 @@ class PurchaseRehearsalExecutor(
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
private fun FreshClickReason.specEntrySubreasonAfterGestureFailure(): String = when (this) {
|
||||
FreshClickReason.SUCCESS -> "gesture_failed_after_action_click_no_effect"
|
||||
FreshClickReason.UNKNOWN -> "gesture_failed_after_unclassified_action_result"
|
||||
else -> specEntrySubreason()
|
||||
}
|
||||
|
||||
private fun selectSpecs(
|
||||
input: PurchaseExecutionInput,
|
||||
rule: PurchaseRule,
|
||||
@@ -814,7 +842,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_POLL_LIMIT = 150
|
||||
private const val PRODUCT_PAGE_POLL_LIMIT = 150
|
||||
private const val PRODUCT_PAGE_STABLE_READS = 2
|
||||
private const val OPEN_PRODUCT_RETRY_POLLS = 10
|
||||
private const val OPEN_PRODUCT_POLL_MILLIS = 100L
|
||||
private const val SPEC_ENTRY_READY_WAIT_POLLS = 20
|
||||
|
||||
@@ -440,15 +440,50 @@ class PurchaseRehearsalExecutorTest {
|
||||
@Test
|
||||
fun `persistent open click failure is bounded and remains safely failed`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
openClickResults = MutableList(10) { FreshActionResult.FAILED },
|
||||
openClickResults = MutableList(20) { FreshActionResult.FAILED },
|
||||
)
|
||||
val pauses = mutableListOf<Long>()
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { "{}" }, pause = pauses::add)
|
||||
.execute(input().copy(phase = "spec_probe"), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("RULE_ACTION_FAILED", outcome.errorCode)
|
||||
assertTrue(driver.openClickCount in 1..5)
|
||||
assertEquals(50, pauses.size)
|
||||
assertTrue(driver.openClickCount in 1..15)
|
||||
assertEquals(150, pauses.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `five second browser interstitial can still reach a stable product page`() {
|
||||
val pauses = mutableListOf<Long>()
|
||||
val driver = FakePurchaseDriver(browserOpenVisibleAfterCaptures = 50)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { "{}" }, pause = pauses::add)
|
||||
.execute(input().copy(phase = "spec_probe"), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("spec_probe_completed", outcome.resultType)
|
||||
assertEquals(1, driver.openClickCount)
|
||||
assertTrue(pauses.count { it == 100L } in 52..60)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pdd foreground without stable product evidence times out at fifteen seconds`() {
|
||||
val pauses = mutableListOf<Long>()
|
||||
val driver = FakePurchaseDriver(loadingPddCaptures = 200)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { "{}" }, pause = pauses::add)
|
||||
.execute(input().copy(phase = "spec_probe"), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("PDD_DETAIL_ENTRY_FAILED", outcome.errorCode)
|
||||
assertEquals("打开拼多多后未识别到稳定商品页面", outcome.message)
|
||||
assertEquals(150, pauses.count { it == 100L })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `explicit pdd login page fails before the product timeout`() {
|
||||
val pauses = mutableListOf<Long>()
|
||||
val driver = FakePurchaseDriver(pddProblemLabels = listOf("手机号登录", "登录后继续"))
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { "{}" }, pause = pauses::add)
|
||||
.execute(input().copy(phase = "spec_probe"), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("PDD_LOGIN_REQUIRED", outcome.errorCode)
|
||||
assertTrue(pauses.count { it == 100L } < 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -505,6 +540,20 @@ class PurchaseRehearsalExecutorTest {
|
||||
assertFalse(driver.panel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed spec entry gesture preserves the action click no effect reason`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
entryActionHasEffect = false,
|
||||
specTapResult = FreshActionResult.FAILED,
|
||||
)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("PURCHASE_SPEC_ENTRY_CLICK_FAILED", outcome.errorCode)
|
||||
assertEquals("gesture_failed_after_action_click_no_effect", outcome.message)
|
||||
assertFalse(outcome.message.contains("unknown"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing spec entry emits scalar source counts without node text`() {
|
||||
val diagnostics = mutableListOf<String>()
|
||||
@@ -523,7 +572,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
"specEntryCandidates=0;explicit=0;nested=0;bottomPurchase=0;panelAlreadyOpen=false;reviewPage=false;pageEvidence=true;entryReadyWaitPolls=20;entryReadyWaitMillis=2000",
|
||||
diagnostics.single(),
|
||||
)
|
||||
assertEquals(20, pauses.count { it == 100L })
|
||||
assertEquals(21, pauses.count { it == 100L })
|
||||
assertFalse(diagnostics.single().contains("选择规格"))
|
||||
}
|
||||
|
||||
@@ -549,7 +598,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertEquals(1, pauses.count { it == 100L })
|
||||
assertEquals(2, pauses.count { it == 100L })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -880,6 +929,8 @@ class PurchaseRehearsalExecutorTest {
|
||||
private val missingSpecEntry: Boolean = false,
|
||||
private val specEntryVisibleAfterPddCaptures: Int = 0,
|
||||
private val loadingPddCaptures: Int = 0,
|
||||
private val browserOpenVisibleAfterCaptures: Int = 0,
|
||||
private val pddProblemLabels: List<String> = emptyList(),
|
||||
private val includeReviewEntry: Boolean = false,
|
||||
private val openReviewOnBottomClick: Boolean = false,
|
||||
private val reviewBackSucceeds: Boolean = true,
|
||||
@@ -929,11 +980,16 @@ class PurchaseRehearsalExecutorTest {
|
||||
private var productEvidenceLost = false
|
||||
private var reviewPage = false
|
||||
private var pddCaptureCount = 0
|
||||
private var browserCaptureCount = 0
|
||||
val clicked = mutableListOf<String>()
|
||||
val clickedPaths = mutableListOf<String>()
|
||||
|
||||
override fun capture(): UiSnapshot {
|
||||
if (browser && !panel && color == null && size == null) {
|
||||
browserCaptureCount++
|
||||
if (browserCaptureCount <= browserOpenVisibleAfterCaptures) {
|
||||
return UiSnapshot("com.heytap.browser", "BrowserActivity", listOf(node("content", "", 0, 0, 1080, 2200)))
|
||||
}
|
||||
val openNodes = mutableListOf(node("open", "打开", 0, 100, 300, 180, clickable = true))
|
||||
if (duplicateOpen) openNodes += node("open2", "打开", 400, 100, 700, 180, clickable = true)
|
||||
openNodes += node("content", "", 0, 0, 1080, 2200)
|
||||
@@ -943,6 +999,11 @@ class PurchaseRehearsalExecutorTest {
|
||||
if (pddCaptureCount <= loadingPddCaptures) {
|
||||
return UiSnapshot(PDD, ACTIVITY, listOf(node("content", "", 0, 0, 1080, 2200)))
|
||||
}
|
||||
if (pddProblemLabels.isNotEmpty()) {
|
||||
return UiSnapshot(PDD, ACTIVITY, pddProblemLabels.mapIndexed { index, label ->
|
||||
node("problem-$index", label, 20, 100 + index * 100, 900, 180 + index * 100)
|
||||
})
|
||||
}
|
||||
if (!panel) {
|
||||
if (reviewPage) {
|
||||
return UiSnapshot(PDD, ACTIVITY, listOf(
|
||||
|
||||
Reference in New Issue
Block a user