fix(purchase): advance past spec confirmation (#220)
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 55
|
||||
versionName = "0.9.42"
|
||||
versionCode = 56
|
||||
versionName = "0.9.43"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+52
@@ -36,6 +36,49 @@ class PurchaseLiveAutomation(
|
||||
private var submitAttempted = false
|
||||
var lastOrderReadFailure: PurchaseOrderReadFailure? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Advances an already verified spec selector to the order confirmation
|
||||
* page. Only the selector's unique exact confirm button is clickable; an
|
||||
* order-submit or payment control can never satisfy this transition.
|
||||
*/
|
||||
fun advanceToOrderConfirmation() {
|
||||
var snapshot = driver.capture()
|
||||
pageProblem(snapshot)
|
||||
if (orderConfirmationReady(snapshot)) return
|
||||
if (snapshot.packageName != PDD_PACKAGE) {
|
||||
fail("PURCHASE_SPEC_CONFIRMATION_NOT_READY", "当前不是拼多多规格页面,未创建订单")
|
||||
}
|
||||
val screen = PddScreenParser.parse(snapshot, PurchaseRehearsalExecutor.DEFAULT_COLLECTOR, "", null)
|
||||
if (screen.specPanelType !in setOf(SpecPanelType.NORMAL_SCROLLABLE, SpecPanelType.NON_SCROLLABLE_CONFIRMATION)) {
|
||||
fail("PURCHASE_SPEC_CONFIRMATION_NOT_READY", "当前规格面板不能安全确认,未创建订单")
|
||||
}
|
||||
val aliases = PurchaseRehearsalExecutor.DEFAULT_COLLECTOR.textAliases.specPanel.confirmAliases
|
||||
.map { it.replace(" ", "") }
|
||||
.toSet()
|
||||
val targets = snapshot.nodes.filter { node ->
|
||||
node.visible && node.enabled && node.clickable && node.label.replace(" ", "") in aliases
|
||||
}.distinctBy { it.path }
|
||||
if (targets.isEmpty()) {
|
||||
fail("PURCHASE_SPEC_CONFIRM_TARGET_MISSING", "没有找到唯一的规格确认按钮,未创建订单")
|
||||
}
|
||||
if (targets.size > 1) {
|
||||
fail("PURCHASE_SPEC_CONFIRM_TARGET_AMBIGUOUS", "规格确认按钮不唯一,未创建订单")
|
||||
}
|
||||
when (driver.clickFresh(targets.single())) {
|
||||
FreshActionResult.SUCCESS -> Unit
|
||||
FreshActionResult.AMBIGUOUS -> fail("PURCHASE_SPEC_CONFIRM_TARGET_AMBIGUOUS", "规格确认按钮不唯一,未创建订单")
|
||||
else -> fail("PURCHASE_SPEC_CONFIRM_CLICK_FAILED", "规格确认按钮点击失败,未创建订单")
|
||||
}
|
||||
repeat(SPEC_CONFIRMATION_MAX_SAMPLES) {
|
||||
pause(SPEC_CONFIRMATION_SAMPLE_INTERVAL_MS)
|
||||
snapshot = driver.capture()
|
||||
pageProblem(snapshot)
|
||||
if (orderConfirmationReady(snapshot)) return
|
||||
}
|
||||
fail("PURCHASE_SPEC_CONFIRMATION_UNCONFIRMED", "规格确认后没有进入订单确认页,未创建订单")
|
||||
}
|
||||
|
||||
fun updateShippingAddress(addressSuffix: String): ShippingAddressProof {
|
||||
if (!addressSuffix.matches(Regex("^_cg[1-9][0-9]*$"))) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "采购任务的地址标记无效")
|
||||
var snapshot = driver.capture()
|
||||
@@ -407,6 +450,13 @@ class PurchaseLiveAutomation(
|
||||
snapshot.nodes.filter { node -> node.visible && node.enabled && FINAL_SUBMIT_MARKERS.any { node.label == it || node.label.startsWith(it) } },
|
||||
)
|
||||
|
||||
private fun orderConfirmationReady(snapshot: UiSnapshot): Boolean {
|
||||
if (snapshot.packageName != PDD_PACKAGE) return false
|
||||
if (snapshot.nodes.any { it.visible && it.enabled && MASKED_PHONE.containsMatchIn(it.label) }) return true
|
||||
return PddScreenParser.parse(snapshot, PurchaseRehearsalExecutor.DEFAULT_COLLECTOR, "", null).specPanelType ==
|
||||
SpecPanelType.ORDER_CONFIRMATION
|
||||
}
|
||||
|
||||
/** A post-submit navigation target is allowed only when it is one exact, non-payment PDD order-detail entry. */
|
||||
private fun orderDetailEntryTargets(snapshot: UiSnapshot): List<SnapshotNode> = uniqueClickable(
|
||||
snapshot,
|
||||
@@ -483,5 +533,7 @@ class PurchaseLiveAutomation(
|
||||
const val ORDER_RESULT_PAYMENT_POST_BACK_MAX_SAMPLES = 3
|
||||
const val ORDER_RESULT_SCROLL_SAMPLE_INTERVAL = 15
|
||||
const val ORDER_RESULT_SAMPLE_INTERVAL_MS = 200L
|
||||
const val SPEC_CONFIRMATION_MAX_SAMPLES = 20
|
||||
const val SPEC_CONFIRMATION_SAMPLE_INTERVAL_MS = 100L
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -105,6 +105,7 @@ class PurchaseRehearsalExecutor(
|
||||
null
|
||||
}
|
||||
PurchaseActionType.UPDATE_SHIPPING_ADDRESS -> try {
|
||||
live.advanceToOrderConfirmation()
|
||||
addressProof = live.updateShippingAddress(input.addressSuffix)
|
||||
null
|
||||
} catch (error: PurchaseLiveException) {
|
||||
|
||||
@@ -16,6 +16,46 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PurchaseLiveAutomationTest {
|
||||
@Test
|
||||
fun `verified spec panel advances through one exact confirm target`() {
|
||||
val driver = SpecConfirmationDriver()
|
||||
|
||||
PurchaseLiveAutomation(driver, pause = {}).advanceToOrderConfirmation()
|
||||
|
||||
assertEquals(listOf("确定"), driver.clicked)
|
||||
assertEquals("order", driver.page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ambiguous missing and unchanged spec confirmation stop safely`() {
|
||||
val ambiguous = SpecConfirmationDriver(confirmLabels = listOf("确定", "确认"))
|
||||
val ambiguousError = runCatching { PurchaseLiveAutomation(ambiguous, pause = {}).advanceToOrderConfirmation() }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
assertEquals("PURCHASE_SPEC_CONFIRM_TARGET_AMBIGUOUS", ambiguousError.code)
|
||||
assertTrue(ambiguous.clicked.isEmpty())
|
||||
|
||||
val missing = SpecConfirmationDriver(confirmLabels = emptyList(), specPanelScrollable = true)
|
||||
val missingError = runCatching { PurchaseLiveAutomation(missing, pause = {}).advanceToOrderConfirmation() }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
assertEquals("PURCHASE_SPEC_CONFIRM_TARGET_MISSING", missingError.code)
|
||||
assertTrue(missing.clicked.isEmpty())
|
||||
|
||||
val unchanged = SpecConfirmationDriver(advanceAfterClick = false)
|
||||
val unchangedError = runCatching { PurchaseLiveAutomation(unchanged, pause = {}).advanceToOrderConfirmation() }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
assertEquals("PURCHASE_SPEC_CONFIRMATION_UNCONFIRMED", unchangedError.code)
|
||||
assertEquals(listOf("确定"), unchanged.clicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existing order confirmation does not click submit or payment controls`() {
|
||||
val driver = SpecConfirmationDriver(startOnOrderPage = true)
|
||||
|
||||
PurchaseLiveAutomation(driver, pause = {}).advanceToOrderConfirmation()
|
||||
|
||||
assertTrue(driver.clicked.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address is retagged verified and final order button can only be clicked once`() {
|
||||
val driver = LiveDriver()
|
||||
@@ -413,6 +453,68 @@ class PurchaseLiveAutomationTest {
|
||||
addressSuffix = "_cg11",
|
||||
)
|
||||
|
||||
private class SpecConfirmationDriver(
|
||||
private val confirmLabels: List<String> = listOf("确定"),
|
||||
private val advanceAfterClick: Boolean = true,
|
||||
private val specPanelScrollable: Boolean = false,
|
||||
startOnOrderPage: Boolean = false,
|
||||
) : PurchaseUiDriver {
|
||||
var page = if (startOnOrderPage) "order" else "spec"
|
||||
val clicked = mutableListOf<String>()
|
||||
|
||||
override fun capture(): UiSnapshot = if (page == "order") {
|
||||
snapshot(listOf(
|
||||
node("root", "", bounds = NodeBounds(0, 0, 1080, 2200)),
|
||||
node("scroll", "", scrollable = true, bounds = NodeBounds(0, 400, 1080, 2100)),
|
||||
node("summary", "已选 黑色 均码"),
|
||||
node("quantity", "1", className = "android.widget.EditText"),
|
||||
node("phone", "138****5678"),
|
||||
node("submit", "提交订单", clickable = true),
|
||||
node("payment", "微信支付"),
|
||||
))
|
||||
} else {
|
||||
snapshot(buildList {
|
||||
add(node("root", "", bounds = NodeBounds(0, 0, 1080, 2200)))
|
||||
if (specPanelScrollable) {
|
||||
add(node("scroll", "", scrollable = true, bounds = NodeBounds(0, 400, 1080, 1500)))
|
||||
}
|
||||
add(node("title", "确认款式"))
|
||||
add(node("summary", "已选 黑色 均码"))
|
||||
val parent = "scroll".takeIf { specPanelScrollable }
|
||||
val prefix = if (specPanelScrollable) "scroll/" else ""
|
||||
add(node("${prefix}color-heading", "颜色分类", parentPath = parent))
|
||||
add(node("${prefix}color", "黑色", clickable = true, parentPath = parent))
|
||||
add(node("${prefix}size-heading", "尺码", parentPath = parent))
|
||||
add(node("${prefix}size", "均码", clickable = true, parentPath = parent))
|
||||
add(node("quantity", "1", className = "android.widget.EditText"))
|
||||
confirmLabels.forEachIndexed { index, label -> add(node("confirm-$index", label, clickable = true)) }
|
||||
})
|
||||
}
|
||||
|
||||
override fun clickFresh(target: SnapshotNode): FreshActionResult {
|
||||
clicked += target.label
|
||||
if (advanceAfterClick && target.label in confirmLabels) page = "order"
|
||||
return FreshActionResult.SUCCESS
|
||||
}
|
||||
|
||||
override fun tapPurchaseFresh(target: SnapshotNode) = FreshActionResult.FAILED
|
||||
override fun inputFresh(target: SnapshotNode, value: String) = FreshActionResult.FAILED
|
||||
override fun swipePurchase(direction: SwipeDirection, durationMs: Long) = false
|
||||
override fun swipePurchaseIn(target: SnapshotNode, direction: SwipeDirection, durationMs: Long) = false
|
||||
override fun backPurchase() = false
|
||||
|
||||
private fun snapshot(nodes: List<SnapshotNode>) = UiSnapshot(PDD, ACTIVITY, nodes)
|
||||
private fun node(
|
||||
path: String,
|
||||
text: String,
|
||||
clickable: Boolean = false,
|
||||
scrollable: Boolean = false,
|
||||
className: String = "android.widget.TextView",
|
||||
bounds: NodeBounds = NodeBounds(20, 100, 900, 180),
|
||||
parentPath: String? = null,
|
||||
) = SnapshotNode(path, parentPath, text, null, null, className, bounds, clickable, scrollable, false, false, true, true)
|
||||
}
|
||||
|
||||
private class LiveDriver(
|
||||
private val addressClipped: Boolean = false,
|
||||
private val duplicatePanels: Boolean = false,
|
||||
|
||||
Reference in New Issue
Block a user