fix(android): block PDD size advice navigation (#189)
This commit is contained in:
+17
-3
@@ -166,12 +166,14 @@ object PddScreenParser {
|
||||
private val excludedOptionWords = listOf(
|
||||
"确定", "确认", "购买", "现在买", "下单", "订单", "支付", "付款",
|
||||
"已选", "请选择", "数量", "打开大图", "查看大图", "一次选多款",
|
||||
"查看尺码建议", "查看尺寸建议", "尺码助手", "尺寸助手", "智能选码", "选码建议",
|
||||
"確定", "確認", "購買", "現在買", "下單", "訂單", "付款",
|
||||
"已選", "請選擇", "數量", "打開大圖", "查看大圖", "一次選多款",
|
||||
"查看尺碼建議", "查看尺寸建議", "尺碼助手", "尺寸助手", "智能選碼", "選碼建議",
|
||||
)
|
||||
private val excludedExactOptionLabels = setOf("客服", "联系客服", "聯繫客服", "店铺", "店鋪", "收藏", "关闭", "關閉")
|
||||
// This denylist is intentionally not configurable: it prevents purchase,
|
||||
// order, and payment controls from ever becoming collection click targets.
|
||||
// These fixed guards are intentionally not configurable: non-spec navigation and
|
||||
// purchase/order/payment controls must never become collection click targets.
|
||||
private val nonConfigurableClickDenylist = listOf("提交订单", "确认订单", "支付", "付款")
|
||||
|
||||
fun parse(snapshot: UiSnapshot, config: PddCollectorConfig, goodsId: String, evidence: PageEvidence?): ParsedPddScreen {
|
||||
@@ -268,7 +270,10 @@ object PddScreenParser {
|
||||
.filter { it.clickable && it.bounds.top >= heading.bounds.bottom && it.bounds.bottom <= lower }
|
||||
.filter { it.bounds.width > 0 && it.bounds.height > 0 && it.label.length <= 80 }
|
||||
.filterNot { isExactHeadingLabel(it.label, config) }
|
||||
.filterNot { node -> isExcludedOptionLabel(node.label) }
|
||||
.filterNot { node ->
|
||||
isExcludedOptionLabel(node.label) ||
|
||||
descendants(node, visibleNodes).any { descendant -> isExcludedOptionLabel(descendant.label) }
|
||||
}
|
||||
.map {
|
||||
val rawText = it.label
|
||||
val normalizedText = when (dimensionKey) {
|
||||
@@ -1542,11 +1547,19 @@ class PddProductDetailCollector(
|
||||
var observedPrice = false
|
||||
var selectionEvidence: String? = null
|
||||
var lastTrace: String? = null
|
||||
var panelMissingReads = 0
|
||||
val baseline = selectionSignature(beforeClick)
|
||||
while (now() <= (priceDeadline ?: selectionDeadline)) {
|
||||
val screen = parse(goodsId, config, evidence)
|
||||
screen.problem?.let { return StablePriceResult(null, it) }
|
||||
if (!screen.pageEvidenceMatched) return StablePriceResult(null, PageProblem("RULE_NOT_MATCHED", "采集期间离开 PDD 商品详情页"))
|
||||
panelMissingReads = if (screen.specPanelOpen) 0 else panelMissingReads + 1
|
||||
if (panelMissingReads >= SPEC_PANEL_LOST_STABLE_READS) {
|
||||
return StablePriceResult(
|
||||
null,
|
||||
PageProblem("SPEC_PANEL_EVIDENCE_NOT_MATCHED", "点击规格后离开了商品规格面板"),
|
||||
)
|
||||
}
|
||||
val targetState = screen.dimensions.flatMap { it.values }.any { it.text == color && (it.node.selected || it.node.checked) }
|
||||
val targetSummary = screen.selectedSummary?.contains(color) == true
|
||||
val panelChanged = selectionSignature(screen) != baseline
|
||||
@@ -1667,6 +1680,7 @@ class PddProductDetailCollector(
|
||||
|
||||
private companion object {
|
||||
const val PRICE_SETTLE_MILLIS = 300L
|
||||
const val SPEC_PANEL_LOST_STABLE_READS = 2
|
||||
const val COLOR_IMAGE_MAX_COUNT = 64
|
||||
const val COLOR_IMAGE_MAX_TOTAL_BYTES = 8 * 1024 * 1024
|
||||
}
|
||||
|
||||
@@ -749,6 +749,29 @@ class PddProductDetailCollectorTest {
|
||||
assertEquals(listOf("红色"), parsed.dimensions.first { it.key == "color" }.values.map { it.text })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sizeAdviceNavigationCannotBecomeAColorValue() {
|
||||
val snapshot = UiSnapshot(
|
||||
PDD_PACKAGE,
|
||||
ACTIVITY,
|
||||
listOf(
|
||||
node("content", "", 0, 0, 1080, 2200, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
|
||||
node("summary", "已选:", 20, 300, 800, 360),
|
||||
node("title", "确认款式", 20, 370, 300, 410),
|
||||
node("color-heading", "颜色分类", 20, 420, 300, 470),
|
||||
node("red-container", "红色", 30, 500, 260, 580, clickable = true),
|
||||
node("red-container/advice", "查看尺码建议", 40, 510, 250, 570, parentPath = "red-container"),
|
||||
node("blue", "蓝色", 290, 500, 520, 580, clickable = true),
|
||||
node("advice", "查看尺码建议", 30, 600, 360, 670, clickable = true),
|
||||
node("confirm", "确定", 0, 2000, 1080, 2150, clickable = true),
|
||||
),
|
||||
)
|
||||
|
||||
val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence())
|
||||
|
||||
assertEquals(listOf("蓝色"), parsed.dimensions.first { it.key == "color" }.values.map { it.text })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun collectsEveryColorPriceAndAllSizesUsingFreshTrees() {
|
||||
val driver = FakeCollectorDriver(
|
||||
@@ -1253,6 +1276,23 @@ class PddProductDetailCollectorTest {
|
||||
assertEquals(listOf("红色"), driver.clickedLabels.distinct())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavingSpecPanelAfterColorClickFailsBeforeSizeDiscovery() {
|
||||
val driver = FakeCollectorDriver(
|
||||
colors = listOf("红色"),
|
||||
prices = mapOf("红色" to 1000L),
|
||||
navigateAwayAfterColorClick = true,
|
||||
)
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }).collect(GOODS_ID, rule())
|
||||
|
||||
assertFalse(result.successful)
|
||||
assertEquals("SPEC_PANEL_EVIDENCE_NOT_MATCHED", result.code)
|
||||
assertEquals(listOf("红色"), driver.clickedLabels)
|
||||
assertTrue(driver.swipes.none { it.first == SwipeDirection.UP })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reviewMisrouteReturnsAndRetriesSpecEntryOnce() {
|
||||
val driver = FakeCollectorDriver(startWithPanelClosed = true, firstEntryOpensReview = true)
|
||||
@@ -1533,6 +1573,7 @@ class PddProductDetailCollectorTest {
|
||||
private val initialSelectedSize: String? = null,
|
||||
private val selectedSummaryPrefix: String = "已选",
|
||||
private val imageColorCards: Boolean = false,
|
||||
private val navigateAwayAfterColorClick: Boolean = false,
|
||||
) : PddCollectorDriver {
|
||||
var captureCount = 0
|
||||
var clickCount = 0
|
||||
@@ -1550,10 +1591,15 @@ class PddProductDetailCollectorTest {
|
||||
private var panelOpen = !startWithPanelClosed
|
||||
private var reviewOpen = false
|
||||
private var quickConfirmationRecovered = false
|
||||
private var adviceOpen = false
|
||||
|
||||
override fun capture(): UiSnapshot {
|
||||
captureCount++
|
||||
fixedSnapshot?.let { return it }
|
||||
if (adviceOpen) return UiSnapshot(PDD_PACKAGE, ACTIVITY, listOf(
|
||||
node("content", "", 0, 0, 1080, 2200, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
|
||||
node("advice-title", "尺码建议", 20, 120, 400, 200),
|
||||
))
|
||||
if (pullDownCount < soldOutUntilPulls) return UiSnapshot(PDD_PACKAGE, ACTIVITY, listOf(
|
||||
node("content", "", 0, 0, 1080, 2200, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
|
||||
node("sold-out", "商品已售罄", 200, 700, 880, 800),
|
||||
@@ -1710,6 +1756,7 @@ class PddProductDetailCollectorTest {
|
||||
previousSelected = selected
|
||||
selected = target.label
|
||||
priceRead = 0
|
||||
if (navigateAwayAfterColorClick) adviceOpen = true
|
||||
return FreshActionResult.SUCCESS
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user