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 c9c2eff..64473ba 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 @@ -375,6 +375,7 @@ object PddScreenParser { val candidates = panelVisible.asSequence() .filter { it.clickable && inValueRegion(it) } .filterNot { isExactHeadingLabel(it.label, config) } + .filterNot { containsDistinctOptionCard(it, sourceByPath) } .filterNot { node -> isExcludedOptionLabel(node.label) || descendants(node, visibleNodes).any { descendant -> isExcludedOptionLabel(descendant.label) } @@ -604,6 +605,7 @@ object PddScreenParser { // Only a block with its own label names the option; a blank row // container resolved from its first child never merges options. if (raw.clickable && raw.label.isNotBlank() && resolved != null && inValueRegion(resolved)) { + if (containsDistinctOptionCard(resolved, sourceByPath)) break val outer = compact(resolved.label) val members = candidates.filter { it.path == resolved.path || it.path.startsWith("${resolved.path}/") } val existingBlock = outer.isNotEmpty() && members.all { compact(it.label).startsWith(outer) } @@ -615,6 +617,18 @@ object PddScreenParser { return block } + /** A labelled group must neither name its children nor become a synthetic selected option. */ + private fun containsDistinctOptionCard(block: SnapshotNode, sourceByPath: Map): Boolean { + val raw = sourceByPath[block.path] ?: return false + if (raw.label.isBlank()) return false + val name = SpecValueNormalizer.normalizeColor(raw.label).filterNot { it.isWhitespace() } + return sourceByPath.values.any { child -> + child.path.startsWith("${block.path}/") && child.clickable && child.label.isNotBlank() && + SpecValueNormalizer.normalizeColor(child.label).filterNot { it.isWhitespace() } != name && + sourceByPath.values.any { it.parentPath == child.path } + } + } + /** * #362: narrowly recognize one full-labelled image above one truncated text * leaf. Do not reverse the old prefix rule for arbitrary nested options. diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/TruncatedSpecCardTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/TruncatedSpecCardTest.kt index 3968849..10aada9 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/TruncatedSpecCardTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/TruncatedSpecCardTest.kt @@ -127,6 +127,35 @@ class TruncatedSpecCardTest { assertTrue(colors(base.copy(nodes = base.nodes + nested)).any { it.text == "黑色" }) } + @Test fun `short labelled common parent does not merge its longer independent cards`() { + val parent = "r/sheet/body/list/color/cards" + val base = sheet(outer = "黑色加绒", caption = "黑色加绒") + val nodes = base.nodes.map { n -> + val isOther = n.path == other || n.path == "$other/img" || n.path == "$other/l/t" + val child = if (isOther) n.copy(text = "黑色", contentDescription = null) else n + child.copy( + path = child.path.replace(card, "$parent/o0").replace(other, "$parent/o1"), + parentPath = child.parentPath?.replace(card, "$parent/o0")?.replace(other, "$parent/o1") + ?.let { if ((n.path == card || n.path == other)) parent else it }, + ) + } + val container = base.nodes.single { it.path == card }.copy( + path = parent, parentPath = parent.substringBeforeLast('/'), text = "黑色", contentDescription = null, + bounds = NodeBounds(36, 1044, 698, 1447), + ) + val values = colors(base.copy(nodes = nodes + container)) + assertEquals(setOf("黑色加绒", "黑色"), values.map { it.text }.toSet()) + assertTrue(values.single { it.text == "黑色加绒" }.node.selected) + assertFalse(values.single { it.text == "黑色" }.node.selected) + } + + @Test fun `same named clickable caption wrapper keeps original badge merging`() { + val snapshot = SpecPanelFixtures.taskOptionDedupSheet().let { it.copy(nodes = it.nodes.map { n -> + if (n.path == "$card/l") n.copy(text = "兰条纹", clickable = true) else n + }) } + assertEquals(listOf("兰条纹", "白条纹"), colors(snapshot).map { it.text }) + } + @Test fun `numeric and weight size values and badge regression remain intact`() { val baseline = SpecPanelFixtures.taskOptionDedupSheet() assertEquals(listOf("兰条纹", "白条纹"), colors(baseline).map { it.text })