Skip to content

Commit be36d69

Browse files
authored
ADFA-4299 | Avoid duplicated ImageView img prefix (#29)
fix(sketch-to-ui): avoid duplicated ImageView id prefixes Improve OCR metadata recovery for generated XML ids
1 parent 550e211 commit be36d69

7 files changed

Lines changed: 233 additions & 67 deletions

File tree

‎sketch-to-ui-plugin/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/cleaner/IdCleaner.kt‎

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,29 @@ package org.appdevforall.codeonthego.computervision.domain.parser.cleaner
22

33
import kotlin.math.abs
44
import me.xdrop.fuzzywuzzy.FuzzySearch
5+
import org.appdevforall.codeonthego.computervision.domain.model.WidgetTypes
56
import org.appdevforall.codeonthego.computervision.domain.parser.ValueCleaner
67
import org.appdevforall.codeonthego.computervision.domain.parser.patterns.AttributeKeyPatterns
78
import org.appdevforall.codeonthego.computervision.domain.parser.patterns.CoreParserPatterns
9+
import org.appdevforall.codeonthego.computervision.domain.parser.patterns.IdPatterns
810
import org.appdevforall.codeonthego.computervision.domain.parser.patterns.ResourceNamePatterns
11+
import org.appdevforall.codeonthego.computervision.domain.xml.AndroidWidgetTags
912

1013
internal object IdCleaner : ValueCleaner {
11-
private const val SWITCH_TAG = "Switch"
12-
private const val IMAGE_VIEW_TAG = "ImageView"
13-
private const val SWITCH_ID_TARGET = "switch"
14-
private const val IMAGE_VIEW_ID_PREFIX = "im"
15-
private const val IMAGE_VIEW_ID_VIEW_TOKEN = "view"
1614
private const val SWITCH_ID_OCR_THRESHOLD = 70
1715
private const val IMAGE_VIEW_ID_OCR_THRESHOLD = 80
1816
private val idVocabulary = listOf(
19-
"cb", "rb", "group", "checkbox", "radio", "btn", "button", "text", "view",
20-
"img", "image", "input", "switch"
17+
"cb", "rb", "group", "checkbox", "radio", "btn", "button", "text", IdPatterns.IMAGE_VIEW_ID_VIEW_TOKEN,
18+
"img", "image", "input", WidgetTypes.SWITCH
2119
)
22-
private val switchIdTargets = listOf(SWITCH_ID_TARGET)
23-
private val imageViewIdPrefixTargets = listOf(IMAGE_VIEW_ID_PREFIX, "img", "image")
24-
private val imageViewIdViewTargets = listOf(IMAGE_VIEW_ID_VIEW_TOKEN)
20+
private val switchIdTargets = listOf(WidgetTypes.SWITCH)
21+
private val imageViewIdPrefixTargets = listOf(IdPatterns.IMAGE_VIEW_ID_PREFIX, "img", "image")
22+
private val imageViewIdViewTargets = listOf(IdPatterns.IMAGE_VIEW_ID_VIEW_TOKEN)
2523

2624
override fun clean(rawValue: String): String {
2725
val cleaned = rawValue.trim().lowercase()
2826
.replace(AttributeKeyPatterns.OCR_IM_OR_M_CONFUSION) { match ->
29-
if (match.value == "inm") "im" else "m"
27+
if (match.value == "inm") IdPatterns.IMAGE_VIEW_ID_PREFIX else "m"
3028
}
3129
.replace(ResourceNamePatterns.RESOURCE_NAME_UNSAFE_CHARS, "_")
3230
.replace(CoreParserPatterns.MULTIPLE_UNDERSCORES, "_")
@@ -39,7 +37,7 @@ internal object IdCleaner : ValueCleaner {
3937
val cleaned = clean(rawValue)
4038
val tokens = cleaned.split('_').filter { it.isNotBlank() }
4139
return normalizeSwitchIdIfNeeded(tokens, tag)
42-
?: normalizeImageViewIdIfNeeded(tokens, tag)
40+
?: normalizeImageViewId(rawValue, tokens, tag)
4341
?: cleaned
4442
}
4543

@@ -65,8 +63,8 @@ internal object IdCleaner : ValueCleaner {
6563
}
6664

6765
private fun normalizeSwitchIdIfNeeded(tokens: List<String>, tag: String): String? {
68-
if (tag != SWITCH_TAG || !isSwitchIdOcrCandidate(tokens)) return null
69-
return buildId(SWITCH_ID_TARGET, extractTrailingNumber(tokens))
66+
if (tag != AndroidWidgetTags.SWITCH || !isSwitchIdOcrCandidate(tokens)) return null
67+
return buildId(WidgetTypes.SWITCH, extractTrailingNumber(tokens))
7068
}
7169

7270
/** Accepts a switch ID candidate when its fuzzy score reaches the switch threshold. */
@@ -75,9 +73,32 @@ internal object IdCleaner : ValueCleaner {
7573
return fuzzyTokenScore(firstToken, switchIdTargets) >= SWITCH_ID_OCR_THRESHOLD
7674
}
7775

78-
private fun normalizeImageViewIdIfNeeded(tokens: List<String>, tag: String): String? {
79-
if (tag != IMAGE_VIEW_TAG || !isImageViewIdOcrCandidate(tokens)) return null
80-
return buildId(IMAGE_VIEW_ID_PREFIX, IMAGE_VIEW_ID_VIEW_TOKEN, extractTrailingNumber(tokens))
76+
private fun normalizeImageViewId(rawValue: String, tokens: List<String>, tag: String): String? {
77+
if (tag != AndroidWidgetTags.IMAGE_VIEW) return null
78+
79+
return removeLeakedImagePrefixIfNeeded(rawValue, tokens)
80+
?: recoverGenericImageViewIdIfNeeded(tokens)
81+
}
82+
83+
private fun removeLeakedImagePrefixIfNeeded(
84+
rawValue: String,
85+
tokens: List<String>
86+
): String? {
87+
if (tokens.size < 3) return null
88+
if (!rawValue.trim().contains(CoreParserPatterns.WHITESPACE)) return null
89+
if (tokens.first() !in imageViewIdPrefixTargets) return null
90+
91+
val remainingTokens = tokens.drop(1)
92+
val remainingId = remainingTokens.joinToString("_")
93+
94+
return remainingId.takeIf {
95+
remainingTokens.firstOrNull() in imageViewIdPrefixTargets
96+
}
97+
}
98+
99+
private fun recoverGenericImageViewIdIfNeeded(tokens: List<String>): String? {
100+
if (!isImageViewIdOcrCandidate(tokens)) return null
101+
return buildId(IdPatterns.IMAGE_VIEW_ID_PREFIX, IdPatterns.IMAGE_VIEW_ID_VIEW_TOKEN, extractTrailingNumber(tokens))
81102
}
82103

83104
private fun isImageViewIdOcrCandidate(tokens: List<String>): Boolean {

‎sketch-to-ui-plugin/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/patterns/IdPatterns.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
package org.appdevforall.codeonthego.computervision.domain.parser.patterns
22

33
internal object IdPatterns {
4+
const val IMAGE_VIEW_ID_PREFIX = "im"
5+
const val IMAGE_VIEW_ID_VIEW_TOKEN = "view"
6+
47
/** Validates an unkeyed OCR fragment before considering it as a corroborated EditText ID. */
58
val BARE_EDIT_TEXT_ID_CANDIDATE = Regex("[A-Za-z][A-Za-z0-9_]{2,}")
69

710
/** Extracts an explicit ID value from ImageView metadata. */
811
val EXPLICIT_LOWER_ID = Regex("\\bid\\s*:\\s*([a-z0-9_-]+)", RegexOption.IGNORE_CASE)
912

13+
/** Matches canonical checkbox group IDs used to derive child CheckBox IDs. */
14+
val CHECKBOX_GROUP_ID = Regex("^cb_group_\\d+$")
15+
16+
/** Matches compact checkbox group OCR variants like `cbgraup2`. */
17+
val COMPACT_CHECKBOX_GROUP_ID = Regex("^cbgr[oa]up?(\\d+)$")
18+
19+
/** Matches RadioButton child IDs that accidentally contain a radio group ID. */
20+
val RADIO_BUTTON_GROUP_CHILD_ID = Regex("^rb_group(?:_\\d+)?(?:_|$).*")
21+
22+
/** Matches RadioButton child IDs that accidentally contain a RadioGroup ID. */
23+
val RADIO_GROUP_CHILD_ID = Regex("^radio_group(?:_\\d+)?(?:_|$).*")
24+
1025
/** Matches a clean `id` key or OCR ID alias followed by a value. */
1126
fun keyedIdValue(idKeyPattern: String): Regex {
1227
return Regex(

‎sketch-to-ui-plugin/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/recovery/EditTextAttributeRecovery.kt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ internal object EditTextAttributeRecovery {
6060
return text.contains(TextPatterns.EDIT_TEXT_METADATA_LEAKAGE)
6161
}
6262

63-
/** Selects a uniquely longest repeated unkeyed ID candidate after password metadata. */
63+
/** Selects an unkeyed ID candidate after password metadata while keeping single-token noise out. */
6464
private fun recoverBareEditTextId(annotation: String): String? {
6565
val parts = annotation.split(PIPE_DELIMITER).map { it.trim() }
6666
val passwordIndex = parts.indexOfFirst { isLikelyPasswordInput(it) }
@@ -76,7 +76,8 @@ internal object EditTextAttributeRecovery {
7676

7777
if (candidates.size < MIN_UNKEYED_ID_CORROBORATION_COUNT) return null
7878
val longestLength = candidates.maxOfOrNull { it.length } ?: return null
79-
return candidates.filter { it.length == longestLength }.singleOrNull()
79+
return candidates.singleOrNull { it.length == longestLength }
80+
?: candidates.firstOrNull()
8081
}
8182

8283
private fun recoverExplicitTextValue(annotation: String): String? {

‎sketch-to-ui-plugin/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidConstants.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ object AndroidConstants {
1313
const val DEFAULT_TEXT_SIZE = "16sp"
1414
}
1515

16+
object AndroidIdLabels {
17+
const val CHECKBOX_GROUP = "cb_group"
18+
const val RADIO_BUTTON = "radio_button"
19+
const val RADIO_GROUP = "radio_group"
20+
const val RADIO_BUTTON_GROUP = "rb_group"
21+
}
22+
1623
object AndroidWidgetTags {
1724
const val LINEAR_LAYOUT = "LinearLayout"
1825
const val RADIO_GROUP = "RadioGroup"

‎sketch-to-ui-plugin/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/WidgetFactory.kt‎

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ package org.appdevforall.codeonthego.computervision.domain.xml
22

33
import org.appdevforall.codeonthego.computervision.domain.model.LayoutItem
44
import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
5+
import org.appdevforall.codeonthego.computervision.domain.model.DetectionLabels
56
import org.appdevforall.codeonthego.computervision.domain.parser.AttributeKey
67
import org.appdevforall.codeonthego.computervision.domain.parser.FuzzyAttributeParser
8+
import org.appdevforall.codeonthego.computervision.domain.parser.patterns.IdPatterns
9+
import org.appdevforall.codeonthego.computervision.domain.parser.patterns.ResourceNamePatterns
710

811
class WidgetFactory(
912
private val context: XmlContext,
1013
private val annotations: Map<ScaledBox, String>,
1114
private val selectedImageOverrides: Map<ScaledBox, String> = emptyMap()
1215
) {
13-
private val checkboxGroupIdPattern = Regex("^cb_group_\\d+$")
1416
private val radioChildGroupIdPatterns = listOf(
15-
Regex("^rb_group(?:_\\d+)?(?:_|$).*"),
16-
Regex("^radio_group(?:_\\d+)?(?:_|$).*")
17+
IdPatterns.RADIO_BUTTON_GROUP_CHILD_ID,
18+
IdPatterns.RADIO_GROUP_CHILD_ID
1719
)
1820

1921
fun createWidgets(item: LayoutItem): List<AndroidWidget> = when (item) {
@@ -40,11 +42,11 @@ class WidgetFactory(
4042
val widgets = mutableListOf<AndroidWidget>()
4143

4244
val dropdownTitle = box.text
43-
.takeIf { box.label == "dropdown" }
45+
.takeIf { box.label == DetectionLabels.DROPDOWN }
4446
?.let(::extractDropdownTitle)
4547

4648
if (dropdownTitle != null) {
47-
val titleBox = box.copy(label = "text", text = dropdownTitle)
49+
val titleBox = box.copy(label = DetectionLabels.TEXT, text = dropdownTitle)
4850

4951
val titleAttrs = mapOf(
5052
AttributeKey.WIDTH.xmlName to AndroidConstants.WRAP_CONTENT,
@@ -71,11 +73,16 @@ class WidgetFactory(
7173

7274
private fun createRadioGroup(item: LayoutItem.RadioGroup): List<AndroidWidget> {
7375
val groupAnnotation = item.boxes.firstNotNullOfOrNull { annotations[it] }
74-
val fullGroupAttrs = FuzzyAttributeParser.parse(groupAnnotation, "RadioGroup")
76+
val fullGroupAttrs = FuzzyAttributeParser.parse(groupAnnotation, AndroidWidgetTags.RADIO_GROUP)
7577

76-
val groupId = resolveRadioGroupId(fullGroupAttrs["android:id"]?.substringAfterLast('/'))
78+
val groupId = resolveRadioGroupId(fullGroupAttrs[AttributeKey.ID.xmlName]?.substringAfterLast('/'))
7779

78-
val groupStructuralAttrs = setOf("android:id", "android:layout_width", "android:layout_height", "android:orientation")
80+
val groupStructuralAttrs = setOf(
81+
AttributeKey.ID.xmlName,
82+
AttributeKey.WIDTH.xmlName,
83+
AttributeKey.HEIGHT.xmlName,
84+
AttributeKey.ORIENTATION.xmlName
85+
)
7986
// Group metadata can contain style attributes; visible option labels must stay with each RadioButton's OCR text.
8087
val sharedAttrs = fullGroupAttrs.filterKeys {
8188
it !in groupStructuralAttrs && it != AttributeKey.TEXT.xmlName
@@ -84,63 +91,68 @@ class WidgetFactory(
8491
var checkedId: String? = null
8592

8693
val children = item.boxes.mapIndexed { index, box ->
87-
val parsedAttrs = (sharedAttrs + FuzzyAttributeParser.parse(annotations[box], "RadioButton")).toMutableMap()
94+
val childAnnotation = annotations[box]
95+
val childAttrs = FuzzyAttributeParser.parse(childAnnotation, AndroidWidgetTags.RADIO_BUTTON)
96+
.filterKeys { key -> childAnnotation != groupAnnotation || key !in groupStructuralAttrs }
97+
val parsedAttrs = (sharedAttrs + childAttrs).toMutableMap()
8898
if (box.text.isNotBlank()) {
8999
parsedAttrs.remove(AttributeKey.TEXT.xmlName)
90100
}
91101

92-
if (parsedAttrs["android:id"] == fullGroupAttrs["android:id"]) {
93-
parsedAttrs.remove("android:id")
102+
if (parsedAttrs[AttributeKey.ID.xmlName] == fullGroupAttrs[AttributeKey.ID.xmlName]) {
103+
parsedAttrs.remove(AttributeKey.ID.xmlName)
94104
}
95105

96-
val requestedId = parsedAttrs["android:id"]?.substringAfterLast('/')
106+
val requestedId = parsedAttrs[AttributeKey.ID.xmlName]?.substringAfterLast('/')
97107
val childId = if (requestedId != null && radioChildGroupIdPatterns.any { it.matches(requestedId) }) {
98-
context.nextId("radio_button")
108+
context.nextId(AndroidIdLabels.RADIO_BUTTON)
99109
} else {
100-
context.resolveId(requestedId, "radio_button")
110+
context.resolveId(requestedId, AndroidIdLabels.RADIO_BUTTON)
101111
}
102112

103-
val isChecked = box.label == "radio_button_checked" || parsedAttrs["android:checked"]?.equals("true", ignoreCase = true) == true
113+
val isChecked = box.label == DetectionLabels.RADIO_BUTTON_CHECKED ||
114+
parsedAttrs[AttributeKey.CHECKED.xmlName]?.equals(AndroidConstants.TRUE, ignoreCase = true) == true
104115
if (isChecked) {
105116
checkedId = childId
106-
parsedAttrs["android:checked"] = "true"
117+
parsedAttrs[AttributeKey.CHECKED.xmlName] = AndroidConstants.TRUE
107118
} else {
108-
parsedAttrs["android:checked"] = "false"
119+
parsedAttrs[AttributeKey.CHECKED.xmlName] = AndroidConstants.FALSE
109120
}
110121

111-
val extraAttrs = if (item.orientation == "horizontal") {
122+
val extraAttrs = if (item.orientation == AndroidConstants.ORIENTATION_HORIZONTAL) {
112123
getMarginEndForHorizontalGap(item.boxes, index)
113124
} else emptyMap()
114125

115126
createSimpleWidget(box, parsedAttrsOverride = parsedAttrs, idOverride = childId, extraAttrs = extraAttrs)
116127
}
117128

118-
val textStyleAttrs = setOf("android:textColor", "android:textSize", "android:textStyle", "android:fontFamily")
129+
val textStyleAttrs = setOf(
130+
AttributeKey.TEXT_COLOR.xmlName,
131+
AttributeKey.TEXT_SIZE.xmlName,
132+
AttributeKey.TEXT_STYLE.xmlName,
133+
AttributeKey.FONT_FAMILY.xmlName
134+
)
119135
val groupFinalAttrs = fullGroupAttrs.filterKeys { it !in textStyleAttrs }.toMutableMap()
120-
groupFinalAttrs["android:id"] = groupId
136+
groupFinalAttrs[AttributeKey.ID.xmlName] = groupId
121137

122138
return listOf(RadioGroupWidget(groupFinalAttrs, children, item.orientation, checkedId))
123139
}
124140

125141
private fun createCheckboxGroup(item: LayoutItem.CheckboxGroup): List<AndroidWidget> {
126142
val groupAnnotation = item.boxes.firstNotNullOfOrNull { annotations[it] }
127-
val parsedAttrs = FuzzyAttributeParser.parse(groupAnnotation, "CheckBox")
143+
val parsedAttrs = FuzzyAttributeParser.parse(groupAnnotation, AndroidWidgetTags.CHECK_BOX)
128144

129-
val requestedId = parsedAttrs["android:id"]?.substringAfterLast('/')
130-
val baseId = if (requestedId != null && checkboxGroupIdPattern.matches(requestedId)) {
131-
context.resolveId(requestedId, "cb_group")
132-
} else {
133-
context.nextId("cb_group", initialIndex = 1)
134-
}
145+
val requestedId = parsedAttrs[AttributeKey.ID.xmlName]?.substringAfterLast('/')
146+
val baseId = resolveCheckboxGroupId(requestedId)
135147

136148
return item.boxes.mapIndexed { index, box ->
137149
val suffix = ('a' + index).toString()
138150
val childId = "${baseId}_$suffix"
139151

140152
val safeAttrs = parsedAttrs.toMutableMap()
141-
safeAttrs.remove("android:id")
153+
safeAttrs.remove(AttributeKey.ID.xmlName)
142154

143-
val extraAttrs = if (item.orientation == "horizontal") {
155+
val extraAttrs = if (item.orientation == AndroidConstants.ORIENTATION_HORIZONTAL) {
144156
getMarginEndForHorizontalGap(item.boxes, index)
145157
} else emptyMap()
146158

@@ -160,7 +172,7 @@ class WidgetFactory(
160172
?: FuzzyAttributeParser.parse(rawAnnotation, tag).toMutableMap()
161173

162174
selectedImageOverrides[box]?.let { drawableReference ->
163-
parsedAttrs["android:src"] = drawableReference
175+
parsedAttrs[AttributeKey.SRC.xmlName] = drawableReference
164176
}
165177

166178
return AndroidWidget.create(box, parsedAttrs).apply {
@@ -175,7 +187,7 @@ class WidgetFactory(
175187
.replace(Regex("^[vV]\\s+"), "")
176188
.trim()
177189

178-
return cleaned.takeIf { it.isNotBlank() && !it.equals("dropdown", ignoreCase = true) }
190+
return cleaned.takeIf { it.isNotBlank() && !it.equals(DetectionLabels.DROPDOWN, ignoreCase = true) }
179191
}
180192

181193
/** Calculates the non-negative horizontal gap to the next box as an end margin. */
@@ -184,18 +196,36 @@ class WidgetFactory(
184196
val currentBox = boxes[currentIndex]
185197
val nextBox = boxes[currentIndex + 1]
186198
val gap = maxOf(0, nextBox.x - (currentBox.x + currentBox.w))
187-
return mapOf("android:layout_marginEnd" to "${gap}dp")
199+
return mapOf(AttributeKey.LAYOUT_MARGIN_END.xmlName to "${gap}dp")
188200
}
189201

190202
private fun resolveRadioGroupId(requestedId: String?): String {
191203
var cleanId = requestedId
192204
if (requestedId != null) {
193205
val normalizedId = requestedId.lowercase()
194206
when {
195-
normalizedId.startsWith("radio_grou") -> cleanId = "radio_group"
196-
normalizedId.startsWith("rb_grou") || normalizedId.startsWith("rb_group") -> cleanId = "rb_group"
207+
normalizedId.startsWith("radio_grou") -> cleanId = AndroidIdLabels.RADIO_GROUP
208+
normalizedId.startsWith("rb_grou") || normalizedId.startsWith(AndroidIdLabels.RADIO_BUTTON_GROUP) ->
209+
cleanId = AndroidIdLabels.RADIO_BUTTON_GROUP
197210
}
198211
}
199-
return context.resolveId(cleanId, "radio_group")
212+
return context.resolveId(cleanId, AndroidIdLabels.RADIO_GROUP)
213+
}
214+
215+
private fun resolveCheckboxGroupId(requestedId: String?): String {
216+
if (requestedId == null) return context.nextId(AndroidIdLabels.CHECKBOX_GROUP, initialIndex = 1)
217+
if (IdPatterns.CHECKBOX_GROUP_ID.matches(requestedId)) {
218+
return context.resolveId(requestedId, AndroidIdLabels.CHECKBOX_GROUP)
219+
}
220+
221+
val compactId = requestedId.lowercase().replace(ResourceNamePatterns.NON_ALPHANUMERIC_LOWER, "")
222+
val recoveredSuffix = IdPatterns.COMPACT_CHECKBOX_GROUP_ID.matchEntire(compactId)?.groupValues?.getOrNull(1)
223+
val cleanId = recoveredSuffix?.let { "${AndroidIdLabels.CHECKBOX_GROUP}_$it" }
224+
225+
return if (cleanId != null) {
226+
context.resolveId(cleanId, AndroidIdLabels.CHECKBOX_GROUP)
227+
} else {
228+
context.nextId(AndroidIdLabels.CHECKBOX_GROUP, initialIndex = 1)
229+
}
200230
}
201231
}

‎sketch-to-ui-plugin/src/test/java/org/appdevforall/codeonthego/computervision/domain/FuzzyAttributeParserTest.kt‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,26 @@ class FuzzyAttributeParserTest {
484484
assertEquals("@drawable/logo", result["android:src"])
485485
}
486486

487+
@Test
488+
fun `Given_leaked_image_prefix_before_image_id_When_parsed_Then_clean_image_id_is_used`() {
489+
val annotation = "height:64do | width:64dp | src:logo.png | icl:img. | img_logo"
490+
val result = FuzzyAttributeParser.parse(annotation, "ImageView")
491+
492+
assertEquals("64dp", result["android:layout_height"])
493+
assertEquals("64dp", result["android:layout_width"])
494+
assertEquals("@drawable/logo", result["android:src"])
495+
assertEquals("img_logo", result["android:id"])
496+
}
497+
498+
@Test
499+
fun `Given_explicit_image_ids_When_parsed_Then_ids_are_not_duplicated_or_rewritten`() {
500+
val ids = listOf("img_logo", "user_email", "rb_flavor", "logo_image", "profile_img")
501+
502+
ids.forEach { id ->
503+
assertEquals(id, FuzzyAttributeParser.parse("id:$id", "ImageView")["android:id"])
504+
}
505+
}
506+
487507
@Test
488508
fun `Given_clean_and_noisy_duplicate_dimensions_When_parsed_Then_clean_dimension_is_preserved`() {
489509
val result = FuzzyAttributeParser.parse(

0 commit comments

Comments
 (0)