diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7532268..5f8903c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "cn.ilapage.goauto.agent" minSdk = 23 targetSdk = 34 - versionCode = 31 - versionName = "0.9.18" + versionCode = 32 + versionName = "0.9.19" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt index d5584ca..cfae3c8 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt @@ -243,6 +243,7 @@ object PddScreenParser { val dimensions = buildList { headings.forEachIndexed { index, heading -> val lower = headings.getOrNull(index + 1)?.bounds?.top ?: Int.MAX_VALUE + val dimensionKey = dimensionKey(heading.label, config, any { it.key == "color" }) val values = panelVisible.asSequence() .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 } @@ -254,7 +255,7 @@ object PddScreenParser { it.label, it.enabled && !stateText.containsUnavailableWord(), it, - colorImageBounds(it, visibleNodes), + if (dimensionKey == "color") colorImageBoundsForOption(it, visibleNodes) else null, ) } .toList() @@ -262,7 +263,7 @@ object PddScreenParser { .map { (_, sameLabel) -> sameLabel.minBy { option -> safeOptionRank(option.node, visibleNodes) } } - if (values.isNotEmpty()) add(VisibleDimension(dimensionKey(heading.label, config, any { it.key == "color" }), heading.label, values)) + if (values.isNotEmpty()) add(VisibleDimension(dimensionKey, heading.label, values)) } } // Opening evidence is intentionally independent from whether the current @@ -363,6 +364,18 @@ object PddScreenParser { val prefix = "${node.path}/" return nodes.filter { it.path.startsWith(prefix) } } + private fun colorImageBoundsForOption(option: SnapshotNode, nodes: List): NodeBounds? { + val byPath = nodes.associateBy(SnapshotNode::path) + val expectedLabel = option.label.replace(Regex("\\s+"), "").takeIf(String::isNotEmpty) ?: return null + var candidate = byPath[option.path] ?: return null + while (true) { + val description = candidate.contentDescription?.replace(Regex("\\s+"), "") + if (description == expectedLabel) { + colorImageBounds(candidate, nodes)?.let { return it } + } + candidate = candidate.parentPath?.let(byPath::get) ?: return null + } + } internal fun colorImageBounds(option: SnapshotNode, nodes: List): NodeBounds? { val expected = option.contentDescription?.replace(Regex("\\s+"), "")?.takeIf(String::isNotEmpty) ?: return null diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt index d0c6807..e985bf8 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt @@ -47,6 +47,78 @@ class PddProductDetailCollectorTest { assertEquals(matching.bounds, PddScreenParser.colorImageBounds(card, listOf(card, matching, globalDuplicate, openLarge))) } + + @Test + fun `full parser locates color image from original card while keeping caption click target`() { + fun describedNode( + path: String, + parent: String?, + text: String?, + description: String?, + bounds: NodeBounds, + className: String, + clickable: Boolean = false, + scrollable: Boolean = false, + ) = SnapshotNode( + path, parent, text, description, null, className, bounds, + clickable, scrollable, selected = false, checked = false, enabled = true, visible = true, + ) + + val targetBounds = NodeBounds(36, 500, 340, 620) + 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, 700, 360), + node("title", "确认款式", 20, 370, 300, 410), + node("color-heading", "颜色分类", 20, 420, 300, 470), + describedNode("scroll", null, null, null, NodeBounds(0, 400, 1080, 1800), "androidx.recyclerview.widget.RecyclerView", scrollable = true), + describedNode("card", "scroll", null, " A色 ", NodeBounds(36, 500, 340, 650), "android.view.ViewGroup", clickable = true), + describedNode("card/image", "card", null, " A色 ", targetBounds, "android.widget.ImageView", clickable = true), + describedNode("card/big", "card", null, "打开大图", NodeBounds(280, 500, 340, 560), "android.widget.ImageView", clickable = true), + describedNode("card/caption", "card", "A色", null, NodeBounds(36, 620, 340, 650), "android.widget.TextView", clickable = true), + describedNode("other/image", "other", null, " A色 ", NodeBounds(382, 500, 698, 620), "android.widget.ImageView", clickable = true), + node("size-heading", "尺码", 20, 700, 300, 750), + node("size", "M", 36, 780, 220, 850, clickable = true), + node("confirm", "确定", 0, 2000, 1080, 2150, clickable = true), + ), + ) + + val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence()) + val color = parsed.dimensions.first { it.key == "color" }.values.single() + val size = parsed.dimensions.first { it.key == "size" }.values.single() + + assertEquals("card/caption", color.node.path) + assertEquals(targetBounds, color.imageBounds) + assertEquals(null, size.imageBounds) + } + + @Test + fun `full parser leaves color image empty when original card has no description`() { + 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, 700, 360), + node("title", "确认款式", 20, 370, 300, 410), + node("color-heading", "颜色分类", 20, 420, 300, 470), + node("scroll", "", 0, 400, 1080, 1800, scrollable = true), + node("card", "A色", 36, 500, 340, 650, clickable = true, parentPath = "scroll", className = "android.view.ViewGroup"), + node("card/image", "A色", 36, 500, 340, 620, clickable = true, parentPath = "card", className = "android.widget.ImageView"), + node("card/caption", "A色", 36, 620, 340, 650, clickable = true, parentPath = "card"), + node("size-heading", "尺码", 20, 700, 300, 750), + node("size", "M", 36, 780, 220, 850, clickable = true), + node("confirm", "确定", 0, 2000, 1080, 2150, clickable = true), + ), + ) + + val color = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence()) + .dimensions.first { it.key == "color" }.values.single() + + assertEquals(null, color.imageBounds) + } @Test fun pageLevelSoldOutRequiresExactTextAndNoNormalProductEvidence() { val soldOut = UiSnapshot(PDD_PACKAGE, ACTIVITY, listOf(