Skip to content

Commit 1e1173d

Browse files
polbinsclaude
andauthored
feat: add accessibility label support for auto capture elements (#343)
* feat: add accessibility label support for auto capture elements * test: add Compose accessibilityLabel test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0f309c3 commit 1e1173d

File tree

7 files changed

+126
-3
lines changed

7 files changed

+126
-3
lines changed

android/src/main/java/com/amplitude/android/internal/ViewTarget.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.amplitude.android.internal
33
import com.amplitude.core.Constants.EventProperties.ACTION
44
import com.amplitude.core.Constants.EventProperties.HIERARCHY
55
import com.amplitude.core.Constants.EventProperties.SCREEN_NAME
6+
import com.amplitude.core.Constants.EventProperties.TARGET_ACCESSIBILITY_LABEL
67
import com.amplitude.core.Constants.EventProperties.TARGET_CLASS
78
import com.amplitude.core.Constants.EventProperties.TARGET_RESOURCE
89
import com.amplitude.core.Constants.EventProperties.TARGET_SOURCE
@@ -23,6 +24,7 @@ data class ViewTarget(
2324
val resourceName: String?,
2425
val tag: String?,
2526
val text: String?,
27+
val accessibilityLabel: String?,
2628
val source: String,
2729
val hierarchy: String?,
2830
internal val ampIgnoreRageClick: Boolean = false,
@@ -56,6 +58,7 @@ fun buildElementInteractedProperties(
5658
TARGET_RESOURCE to target.resourceName,
5759
TARGET_TAG to target.tag,
5860
TARGET_TEXT to target.text,
61+
TARGET_ACCESSIBILITY_LABEL to target.accessibilityLabel,
5962
TARGET_SOURCE to
6063
target.source
6164
.replace("_", " ")

android/src/main/java/com/amplitude/android/internal/locators/AndroidViewTargetLocator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal class AndroidViewTargetLocator : ViewTargetLocator {
3131
?.takeIf { it is String || it is Number || it is Boolean || it is Char }
3232
?.toString()
3333
val text = (this as? Button)?.text?.toString()
34+
val accessibilityLabel = contentDescription?.toString()
3435

3536
// Read frustration analytics settings from programmatic tags (and Compose)
3637
val frustrationSettings = readFrustrationAttributes()
@@ -41,6 +42,7 @@ internal class AndroidViewTargetLocator : ViewTargetLocator {
4142
resourceName,
4243
tag,
4344
text,
45+
accessibilityLabel,
4446
SOURCE,
4547
hierarchy,
4648
ampIgnoreRageClick = frustrationSettings.ignoreRageClick,

android/src/main/java/com/amplitude/android/internal/locators/ComposeViewTargetLocator.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ internal class ComposeViewTargetLocator(private val logger: Logger) : ViewTarget
3333
// the final tag to return (can be null if no test tag is found)
3434
var targetTag: String? = null
3535

36+
// the final accessibility label to return
37+
var targetAccessibilityLabel: String? = null
38+
3639
// track if we found a clickable element
3740
var foundClickableElement = false
3841

3942
// the last known tag when iterating the node tree
4043
var lastKnownTag: String? = null
4144

45+
// the last known accessibility label when iterating the node tree
46+
var lastKnownAccessibilityLabel: String? = null
47+
4248
// Amplitude frustration analytics configuration (extracted as simple booleans)
4349
var ignoreRageClick = false
4450
var ignoreDeadClick = false
@@ -78,9 +84,17 @@ internal class ComposeViewTargetLocator(private val logger: Logger) : ViewTarget
7884
val elementValue = element.value
7985
if (elementValue is LinkedHashMap<*, *>) {
8086
for ((key, value) in elementValue.entries) {
81-
if (key == "TestTag") {
82-
lastKnownTag = value as? String
83-
break
87+
when (key) {
88+
"TestTag" -> {
89+
lastKnownTag = value as? String
90+
}
91+
"ContentDescription" -> {
92+
// ContentDescription is a List<String> in Compose semantics
93+
lastKnownAccessibilityLabel =
94+
(value as? List<*>)
95+
?.filterIsInstance<String>()
96+
?.joinToString(", ")
97+
}
8498
}
8599
}
86100
}
@@ -105,6 +119,7 @@ internal class ComposeViewTargetLocator(private val logger: Logger) : ViewTarget
105119
if (isClickable && targetType == ViewTarget.Type.Clickable) {
106120
foundClickableElement = true
107121
targetTag = lastKnownTag // can be null if no test tag is found
122+
targetAccessibilityLabel = lastKnownAccessibilityLabel // can be null
108123
}
109124
}
110125
queue.addAll(node.zSortedChildren.asMutableList())
@@ -119,6 +134,7 @@ internal class ComposeViewTargetLocator(private val logger: Logger) : ViewTarget
119134
resourceName = null,
120135
tag = targetTag,
121136
text = null,
137+
accessibilityLabel = targetAccessibilityLabel,
122138
source = SOURCE,
123139
hierarchy = null,
124140
ampIgnoreRageClick = ignoreRageClick,

android/src/test/kotlin/com/amplitude/android/internal/ViewTargetTest.kt

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.amplitude.android.internal
22

3+
import com.amplitude.core.Constants.EventProperties.TARGET_ACCESSIBILITY_LABEL
4+
import org.junit.jupiter.api.Assertions.assertEquals
35
import org.junit.jupiter.api.Assertions.assertFalse
6+
import org.junit.jupiter.api.Assertions.assertNull
47
import org.junit.jupiter.api.Assertions.assertTrue
58
import org.junit.jupiter.api.Test
69

@@ -14,6 +17,7 @@ class ViewTargetTest {
1417
resourceName = "test_resource",
1518
tag = "normal_tag",
1619
text = "Test",
20+
accessibilityLabel = null,
1721
source = "android_view",
1822
hierarchy = "Activity → TestView",
1923
ampIgnoreRageClick = false,
@@ -33,6 +37,7 @@ class ViewTargetTest {
3337
// Regular tag, not used for frustration analytics
3438
tag = "some_other_tag",
3539
text = "Test",
40+
accessibilityLabel = null,
3641
source = "android_view",
3742
hierarchy = "Activity → TestView",
3843
ampIgnoreRageClick = true,
@@ -51,6 +56,7 @@ class ViewTargetTest {
5156
resourceName = null,
5257
tag = null,
5358
text = "Compose Button",
59+
accessibilityLabel = null,
5460
source = "jetpack_compose",
5561
hierarchy = "Activity → ComposableScreen → Button",
5662
ampIgnoreRageClick = true,
@@ -69,6 +75,7 @@ class ViewTargetTest {
6975
resourceName = null,
7076
tag = null,
7177
text = "Icon Button",
78+
accessibilityLabel = null,
7279
source = "jetpack_compose",
7380
hierarchy = "Activity → ComposableScreen → IconButton",
7481
ampIgnoreRageClick = true,
@@ -87,6 +94,7 @@ class ViewTargetTest {
8794
resourceName = null,
8895
tag = null,
8996
text = "FAB",
97+
accessibilityLabel = null,
9098
source = "jetpack_compose",
9199
hierarchy = "Activity → ComposableScreen → FloatingActionButton",
92100
ampIgnoreRageClick = false,
@@ -105,6 +113,7 @@ class ViewTargetTest {
105113
resourceName = null,
106114
tag = null,
107115
text = "Input Field",
116+
accessibilityLabel = null,
108117
source = "jetpack_compose",
109118
hierarchy = "Activity → ComposableScreen → TextField",
110119
ampIgnoreRageClick = false,
@@ -123,6 +132,7 @@ class ViewTargetTest {
123132
resourceName = "test_resource",
124133
tag = null,
125134
text = "Test View",
135+
accessibilityLabel = null,
126136
source = "android_view",
127137
hierarchy = "Activity → TestView",
128138
ampIgnoreRageClick = true,
@@ -141,6 +151,7 @@ class ViewTargetTest {
141151
resourceName = "test_resource",
142152
tag = null,
143153
text = "Test View",
154+
accessibilityLabel = null,
144155
source = "android_view",
145156
hierarchy = "Activity → TestView",
146157
ampIgnoreRageClick = true,
@@ -159,6 +170,7 @@ class ViewTargetTest {
159170
resourceName = "test_resource",
160171
tag = null,
161172
text = "Test View",
173+
accessibilityLabel = null,
162174
source = "android_view",
163175
hierarchy = "Activity → TestView",
164176
ampIgnoreRageClick = false,
@@ -188,6 +200,7 @@ class ViewTargetTest {
188200
resourceName = null,
189201
tag = null,
190202
text = text,
203+
accessibilityLabel = null,
191204
source = "jetpack_compose",
192205
hierarchy = hierarchy,
193206
ampIgnoreRageClick = true,
@@ -206,6 +219,7 @@ class ViewTargetTest {
206219
resourceName = null,
207220
tag = null,
208221
text = text,
222+
accessibilityLabel = null,
209223
source = "jetpack_compose",
210224
hierarchy = hierarchy,
211225
ampIgnoreRageClick = false,
@@ -217,4 +231,87 @@ class ViewTargetTest {
217231
)
218232
}
219233
}
234+
235+
@Test
236+
fun `buildElementInteractedProperties - includes accessibilityLabel when set`() {
237+
val viewTarget =
238+
ViewTarget(
239+
_view = null,
240+
className = "android.widget.Button",
241+
resourceName = "submit_button",
242+
tag = null,
243+
text = "Submit",
244+
accessibilityLabel = "Submit form button",
245+
source = "android_view",
246+
hierarchy = "Activity → Button",
247+
ampIgnoreRageClick = false,
248+
ampIgnoreDeadClick = false,
249+
)
250+
251+
val properties = buildElementInteractedProperties(viewTarget, "MainActivity")
252+
253+
assertEquals("Submit form button", properties[TARGET_ACCESSIBILITY_LABEL])
254+
}
255+
256+
@Test
257+
fun `buildElementInteractedProperties - accessibilityLabel is null when not set`() {
258+
val viewTarget =
259+
ViewTarget(
260+
_view = null,
261+
className = "android.widget.Button",
262+
resourceName = "submit_button",
263+
tag = null,
264+
text = "Submit",
265+
accessibilityLabel = null,
266+
source = "android_view",
267+
hierarchy = "Activity → Button",
268+
ampIgnoreRageClick = false,
269+
ampIgnoreDeadClick = false,
270+
)
271+
272+
val properties = buildElementInteractedProperties(viewTarget, "MainActivity")
273+
274+
assertNull(properties[TARGET_ACCESSIBILITY_LABEL])
275+
}
276+
277+
@Test
278+
fun `accessibilityLabel - stores contentDescription value correctly`() {
279+
val viewTarget =
280+
ViewTarget(
281+
_view = null,
282+
className = "android.widget.ImageButton",
283+
resourceName = "close_button",
284+
tag = null,
285+
text = null,
286+
accessibilityLabel = "Close dialog",
287+
source = "android_view",
288+
hierarchy = "Activity → Dialog → ImageButton",
289+
ampIgnoreRageClick = false,
290+
ampIgnoreDeadClick = false,
291+
)
292+
293+
assertEquals("Close dialog", viewTarget.accessibilityLabel)
294+
}
295+
296+
@Test
297+
fun `buildElementInteractedProperties - includes accessibilityLabel for Compose element`() {
298+
val viewTarget =
299+
ViewTarget(
300+
_view = null,
301+
className = null,
302+
resourceName = null,
303+
tag = "submit_button",
304+
text = null,
305+
accessibilityLabel = "Submit form",
306+
source = "jetpack_compose",
307+
hierarchy = null,
308+
ampIgnoreRageClick = false,
309+
ampIgnoreDeadClick = false,
310+
)
311+
312+
val properties = buildElementInteractedProperties(viewTarget, "ComposeActivity")
313+
314+
assertEquals("Submit form", properties[TARGET_ACCESSIBILITY_LABEL])
315+
assertEquals("Jetpack Compose", properties["[Amplitude] Target Source"])
316+
}
220317
}

android/src/test/kotlin/com/amplitude/android/internal/gestures/AutocaptureGestureListenerClickTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class AutocaptureGestureListenerClickTest {
148148
"[Amplitude] Target Resource" to "test_button",
149149
"[Amplitude] Target Tag" to null,
150150
"[Amplitude] Target Text" to null,
151+
"[Amplitude] Target Accessibility Label" to null,
151152
"[Amplitude] Target Source" to "Android View",
152153
"[Amplitude] Hierarchy" to "View",
153154
"[Amplitude] Screen Name" to "test_screen",

android/src/test/kotlin/com/amplitude/android/internal/gestures/ViewMockHelper.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal fun <T : View> mockView(
4949
every { mockView.context } returns context
5050
every { mockView.isClickable } returns clickable
5151
every { mockView.visibility } returns if (visible) View.VISIBLE else View.GONE
52+
every { mockView.contentDescription } returns null
5253

5354
every { mockView.getLocationOnScreen(any()) } answers {
5455
val array = invocation.args[0] as IntArray

core/src/main/java/com/amplitude/core/Constants.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ object Constants {
5959
const val NETWORK_TRACKING_REQUEST_BODY_SIZE = "[Amplitude] Request Body Size"
6060
const val NETWORK_TRACKING_RESPONSE_BODY_SIZE = "[Amplitude] Response Body Size"
6161

62+
// Accessibility properties
63+
const val TARGET_ACCESSIBILITY_LABEL = "[Amplitude] Target Accessibility Label"
64+
6265
// Frustration interactions properties
6366
const val BEGIN_TIME = "[Amplitude] Begin Time"
6467
const val END_TIME = "[Amplitude] End Time"

0 commit comments

Comments
 (0)