fix(t210): recognize current Pinduoduo result pages
This commit is contained in:
@@ -594,7 +594,7 @@ class MainActivity : ComponentActivity() {
|
||||
searchAutomation = PinduoduoSearchAutomation(
|
||||
driver = AndroidPinduoduoUiDriver(this),
|
||||
keyword = searchKeyword,
|
||||
forceKeywordEntry = boundRequirement != null
|
||||
forceKeywordEntry = true
|
||||
),
|
||||
candidateAutomation = candidateAutomation
|
||||
)
|
||||
|
||||
+10
-3
@@ -110,9 +110,16 @@ class BuyerAccessibilityService : AccessibilityService() {
|
||||
node.contentDescription?.toString()?.trim() == "搜索"
|
||||
PinduoduoPage.SEARCH_RESULTS,
|
||||
PinduoduoPage.SEARCH_RESULTS_OTHER_QUERY ->
|
||||
node.className?.toString()
|
||||
?.endsWith("HorizontalScrollView") == true &&
|
||||
node.contentDescription?.toString()?.trim() == "搜索"
|
||||
node.contentDescription?.toString()?.trim() == "搜索" &&
|
||||
(
|
||||
node.className?.toString()
|
||||
?.endsWith("HorizontalScrollView") == true ||
|
||||
(
|
||||
node.className?.toString()
|
||||
?.endsWith("TextView") == true &&
|
||||
!node.text.isNullOrBlank()
|
||||
)
|
||||
)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
+70
-5
@@ -32,6 +32,20 @@ data class PinduoduoUiSnapshot(
|
||||
)
|
||||
|
||||
object PinduoduoPageClassifier {
|
||||
private val legacyResultSortMarkers = setOf("综合", "销量", "价格", "筛选")
|
||||
private val modernResultCategoryMarkers = setOf(
|
||||
"推荐",
|
||||
"综合",
|
||||
"销量",
|
||||
"价格",
|
||||
"筛选",
|
||||
"手机",
|
||||
"女装",
|
||||
"百货",
|
||||
"海淘",
|
||||
"母婴",
|
||||
"车品"
|
||||
)
|
||||
private val paymentMarkers = listOf(
|
||||
"确认订单",
|
||||
"提交订单",
|
||||
@@ -86,13 +100,30 @@ object PinduoduoPageClassifier {
|
||||
normalize(element.contentDescription.orEmpty()) == "搜索"
|
||||
}
|
||||
val hasHome = normalized.any { it == "首页" }
|
||||
val hasResultSearchHeader = visibleElements.any { element ->
|
||||
val hasLegacyResultSearchHeader = visibleElements.any { element ->
|
||||
element.className.endsWith("HorizontalScrollView") &&
|
||||
normalize(element.contentDescription.orEmpty()) == "搜索"
|
||||
}
|
||||
val sortControlCount = setOf("综合", "销量", "价格", "筛选")
|
||||
val hasModernResultSearchHeader = visibleElements.any { element ->
|
||||
element.className.endsWith("TextView") &&
|
||||
normalize(element.contentDescription.orEmpty()) == "搜索" &&
|
||||
!element.text.isNullOrBlank()
|
||||
}
|
||||
val hasResultSearchHeader =
|
||||
hasLegacyResultSearchHeader || hasModernResultSearchHeader
|
||||
val legacySortControlCount = legacyResultSortMarkers
|
||||
.count { marker -> normalized.any { it == marker } }
|
||||
val hasExactQuery = visibleTexts.any { it.trim() == expectedQuery }
|
||||
val modernCategoryCount = modernResultCategoryMarkers
|
||||
.count { marker -> normalized.any { it == marker } }
|
||||
val hasHorizontalCategoryBar = visibleElements.any { element ->
|
||||
element.className.endsWith("HorizontalScrollView")
|
||||
}
|
||||
val hasResultControls =
|
||||
legacySortControlCount >= 3 ||
|
||||
(hasHorizontalCategoryBar && modernCategoryCount >= 3)
|
||||
val hasExpectedQuery = visibleTexts.any { text ->
|
||||
matchesExpectedQuery(text, expectedQuery)
|
||||
}
|
||||
val hasDetailBack = visibleElements.any { element ->
|
||||
element.clickable &&
|
||||
normalize(element.contentDescription.orEmpty()) == "返回"
|
||||
@@ -101,9 +132,9 @@ object PinduoduoPageClassifier {
|
||||
.count { marker -> normalized.any { it.contains(marker) } }
|
||||
|
||||
val page = when {
|
||||
hasExactQuery && hasResultSearchHeader && sortControlCount >= 3 ->
|
||||
hasExpectedQuery && hasResultSearchHeader && hasResultControls ->
|
||||
PinduoduoPage.SEARCH_RESULTS
|
||||
hasResultSearchHeader && sortControlCount >= 3 ->
|
||||
hasResultSearchHeader && hasResultControls ->
|
||||
PinduoduoPage.SEARCH_RESULTS_OTHER_QUERY
|
||||
hasSearchInput && hasSubmitButton -> PinduoduoPage.SEARCH_INPUT
|
||||
hasHomeSearchEntry && hasHome -> PinduoduoPage.HOME
|
||||
@@ -120,6 +151,40 @@ object PinduoduoPageClassifier {
|
||||
private fun normalize(value: String): String =
|
||||
value.lowercase().replace(Regex("\\s+"), "")
|
||||
|
||||
private fun matchesExpectedQuery(observed: String, expected: String): Boolean {
|
||||
val normalizedObserved = normalize(observed)
|
||||
val normalizedExpected = normalize(expected)
|
||||
if (normalizedObserved == normalizedExpected) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
normalizedObserved.length >= MINIMUM_QUERY_FRAGMENT_LENGTH &&
|
||||
normalizedExpected.contains(normalizedObserved)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
val fragments = normalizedObserved
|
||||
.replace("...", "…")
|
||||
.split('…')
|
||||
.filter { it.length >= MINIMUM_ELIDED_FRAGMENT_LENGTH }
|
||||
if (fragments.size < 2) {
|
||||
return false
|
||||
}
|
||||
var nextIndex = 0
|
||||
for (fragment in fragments) {
|
||||
val index = normalizedExpected.indexOf(fragment, nextIndex)
|
||||
if (index < 0) {
|
||||
return false
|
||||
}
|
||||
nextIndex = index + fragment.length
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Collection<String>.containsAny(markers: Collection<String>): Boolean =
|
||||
any { text -> markers.any(text::contains) }
|
||||
|
||||
private const val MINIMUM_QUERY_FRAGMENT_LENGTH = 12
|
||||
private const val MINIMUM_ELIDED_FRAGMENT_LENGTH = 4
|
||||
}
|
||||
|
||||
+53
@@ -49,6 +49,59 @@ class PinduoduoPageClassifierTest {
|
||||
assertEquals(PinduoduoPage.SEARCH_RESULTS, snapshot.page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modern 817 result layout identifies search results`() {
|
||||
val snapshot = classify(
|
||||
element(
|
||||
text = SEARCH_PROBE_KEYWORD,
|
||||
contentDescription = "搜索"
|
||||
),
|
||||
element(className = "android.widget.HorizontalScrollView"),
|
||||
element(text = "推荐"),
|
||||
element(text = "手机"),
|
||||
element(text = "女装"),
|
||||
element(text = "百货")
|
||||
)
|
||||
|
||||
assertEquals(PinduoduoPage.SEARCH_RESULTS, snapshot.page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `elided result query identifies long submitted search`() {
|
||||
val submittedQuery =
|
||||
"現貨+預購 有口袋直筒褲 長褲 褲子 高腰直筒褲 春夏秋長褲 高腰寬褲 黑色長褲 " +
|
||||
"高腰長褲 顯瘦西裝褲 遮肚女裝 冰絲褲 直筒褲(夏季薄款有口袋),9XL【建議130-150公斤】(預購)"
|
||||
val snapshot = PinduoduoPageClassifier.classify(
|
||||
foregroundPackage = PINDUODUO_PACKAGE,
|
||||
elements = listOf(
|
||||
element(
|
||||
text = "現貨+預購 有…斤】(預購)",
|
||||
contentDescription = "搜索"
|
||||
),
|
||||
element(className = "android.widget.HorizontalScrollView"),
|
||||
element(text = "推荐"),
|
||||
element(text = "手机"),
|
||||
element(text = "女装")
|
||||
),
|
||||
expectedQuery = submittedQuery
|
||||
)
|
||||
|
||||
assertEquals(PinduoduoPage.SEARCH_RESULTS, snapshot.page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modern result layout with another query remains distinct`() {
|
||||
val snapshot = classify(
|
||||
element(text = "直筒裤有口袋女", contentDescription = "搜索"),
|
||||
element(className = "android.widget.HorizontalScrollView"),
|
||||
element(text = "推荐"),
|
||||
element(text = "手机"),
|
||||
element(text = "女装")
|
||||
)
|
||||
|
||||
assertEquals(PinduoduoPage.SEARCH_RESULTS_OTHER_QUERY, snapshot.page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login verification and risk markers remain distinct`() {
|
||||
assertEquals(
|
||||
|
||||
Reference in New Issue
Block a user