-
Notifications
You must be signed in to change notification settings - Fork 83
Feature/compose click node name from modifier #1415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a49f162
851c7f5
b5fa694
32e5d82
a61d158
620245c
0bd5f79
b158794
7f48f3c
37187f0
7d1f5bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,24 +67,25 @@ internal class ComposeTapTargetDetector( | |
| ): LayoutNode? { | ||
| val queue = LinkedList<LayoutNode>() | ||
| queue.addFirst(owner.root) | ||
| var target: LayoutNode? = null | ||
|
|
||
| while (queue.isNotEmpty()) { | ||
| val node = queue.removeFirst() | ||
| if (node.isPlaced && hitTest(node, x, y)) { | ||
| target = node | ||
| return node | ||
| } | ||
|
|
||
| queue.addAll(node.zSortedChildren.asMutableList()) | ||
| } | ||
| return target | ||
| return null | ||
|
Comment on lines
-70
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At first glance, this appears to be a semantic change. Previously, the code would keep adding children and walking the queue until it found the "last" one that matched -- which I think means that it potentially got more specific. So was this change intentional, and if so, why?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes this change was intentional. For me it seems to be a bug to take the last node. In my test application when a compose component consists of multiple nodes (i.e. i'm using some UI components from some 3rd party library), this code caused to take the node from the library, but not the node that I define in my application. In my opinion it should get the top most node. |
||
| } | ||
|
|
||
| private fun isValidClickTarget(node: LayoutNode): Boolean { | ||
| for (info in node.getModifierInfo()) { | ||
| val modifier = info.modifier | ||
| if (modifier is SemanticsModifier) { | ||
| with(modifier.semanticsConfiguration) { | ||
| if (contains(OpentelemetrySemanticsPropertyKey)) { | ||
| return true | ||
| } | ||
| if (contains(SemanticsActions.OnClick)) { | ||
| return true | ||
| } | ||
|
|
@@ -110,6 +111,11 @@ internal class ComposeTapTargetDetector( | |
| val modifier = info.modifier | ||
| if (modifier is SemanticsModifier) { | ||
| with(modifier.semanticsConfiguration) { | ||
| val opentelemetrySemanticsPropertyKey = getOrNull(OpentelemetrySemanticsPropertyKey) | ||
| if (!opentelemetrySemanticsPropertyKey.isNullOrBlank()) { | ||
| return opentelemetrySemanticsPropertyKey | ||
| } | ||
|
|
||
| val onClickSemanticsConfiguration = getOrNull(SemanticsActions.OnClick) | ||
| if (onClickSemanticsConfiguration != null) { | ||
| val accessibilityActionLabel = onClickSemanticsConfiguration.label | ||
|
|
@@ -131,6 +137,10 @@ internal class ComposeTapTargetDetector( | |
| } else { | ||
| className = modifier::class.qualifiedName | ||
| } | ||
| val testTag = modifier.getTestTag() | ||
| if (testTag != null) { | ||
| return testTag | ||
| } | ||
| } | ||
|
|
||
| return className | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.instrumentation.compose.click | ||
|
|
||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.semantics.SemanticsPropertyKey | ||
| import androidx.compose.ui.semantics.SemanticsPropertyReceiver | ||
| import androidx.compose.ui.semantics.semantics | ||
|
|
||
| /** | ||
| * An opentelemetry Modifier that allows to mark a composable element as traceable. | ||
| * When a composable element that has this Modifier will be tapped, then the [name] from this | ||
| * Modifier will be taken as the element name | ||
| * | ||
| * @param name name of the tapped element | ||
| */ | ||
| fun Modifier.opentelemetry(name: String): Modifier = | ||
| this.semantics { | ||
| this.opentelemetry = name | ||
| } | ||
|
|
||
| internal val OpentelemetrySemanticsPropertyKey: SemanticsPropertyKey<String> = | ||
| SemanticsPropertyKey( | ||
| name = "_opentelemetry_semantics", | ||
| mergePolicy = { parentValue, _ -> | ||
| parentValue | ||
| }, | ||
| ) | ||
|
|
||
| private var SemanticsPropertyReceiver.opentelemetry by OpentelemetrySemanticsPropertyKey |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.instrumentation.compose.click | ||
|
|
||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.semantics.SemanticsModifier | ||
| import androidx.compose.ui.semantics.SemanticsProperties | ||
| import androidx.compose.ui.semantics.getOrNull | ||
| import kotlin.jvm.java | ||
| import kotlin.text.isNullOrEmpty | ||
|
|
||
| private const val TEST_TAG_FIELD_NAME = "tag" | ||
|
|
||
| internal fun Modifier.getTestTag(): String? = findTestTagInModifier(this) | ||
|
|
||
| internal fun findTestTagInModifier(modifier: Modifier): String? { | ||
| if (modifier is SemanticsModifier) { | ||
| with(modifier.semanticsConfiguration) { | ||
| val testTag = getOrNull(SemanticsProperties.TestTag) | ||
| if (!testTag.isNullOrEmpty()) { | ||
| return testTag | ||
| } | ||
| } | ||
| } | ||
| // Often the Modifier is a TestTagElement. As this class is private there is only a way to | ||
| // get the TestTag value using reflection | ||
| if ("androidx.compose.ui.platform.TestTagElement" == modifier::class.qualifiedName) { | ||
| try { | ||
| val testTagField = modifier::class.java.getDeclaredField(TEST_TAG_FIELD_NAME) | ||
| testTagField.isAccessible = true | ||
| val testTag = testTagField.get(modifier) as String | ||
| if (testTag.isNotEmpty()) { | ||
| return testTag | ||
| } | ||
| } catch (_: Exception) { | ||
| } | ||
| } | ||
| return null | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package androidx.compose.ui.platform | ||
|
|
||
| import androidx.compose.ui.Modifier | ||
|
|
||
| // Test-only stand-in for the private Compose TestTagElement. | ||
| // Must have a private field named `tag` so reflection in the production code finds it. | ||
| class TestTagElement( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting. Are users supposed to be able to consume this, because I don't think that this would be published in any consumable artifacts. Maybe it should just be in the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for internal unit test of opentelemetry-android compose-click module |
||
| @Suppress("UnusedPrivateProperty") private val tag: String, | ||
| ) : Modifier.Element | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -235,6 +235,8 @@ internal class ComposeClickEventGeneratorTest { | |
|
|
||
| every { semanticsModifier.semanticsConfiguration } returns semanticsConfiguration | ||
| every { semanticsConfiguration.contains(eq(SemanticsActions.OnClick)) } returns true | ||
| every { semanticsConfiguration.contains(eq(OpentelemetrySemanticsPropertyKey)) } returns false | ||
| every { semanticsConfiguration.getOrNull(eq(OpentelemetrySemanticsPropertyKey)) } returns null | ||
|
|
||
| if (useDescription) { | ||
| every { semanticsConfiguration.getOrNull(eq(SemanticsActions.OnClick)) } returns null | ||
|
|
@@ -284,7 +286,7 @@ internal class ComposeClickEventGeneratorTest { | |
| } | ||
|
|
||
| every { nodeList[0].zSortedChildren } returns mutableVectorOf(nodeList[1], nodeList[2]) | ||
| every { nodeList[1].zSortedChildren } returns mutableVectorOf(nodeList[4], nodeList[3]) | ||
| every { nodeList[1].zSortedChildren } returns mutableVectorOf(nodeList[3], nodeList[4]) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wondering what triggered this change because the idea is that 3 is higher than 4 in z-order and swapping them means 4 is now on top and the assertion on lines
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me check the unit tests again |
||
| every { nodeList[2].zSortedChildren } returns mutableVectorOf() | ||
|
|
||
| every { nodeList[3].zSortedChildren } returns mutableVectorOf() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be internal to avoid exposing it to anybody using this module, or does it need to be public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has to be public as this is meant to be used on jetpack compose elements defined by application