Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
package com.datadog.android.compose

import androidx.compose.ui.Modifier
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.SemanticsModifierNode
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.semantics
import com.datadog.android.compose.internal.InstrumentationType
import com.datadog.android.compose.internal.sendTelemetry

Expand All @@ -28,7 +30,7 @@ import com.datadog.android.compose.internal.sendTelemetry
*/
fun Modifier.datadog(name: String, isImage: Boolean = false): Modifier {
sendTelemetry(autoInstrumented = false, InstrumentationType.Semantics)
return this.datadogSemantics(name, isImage)
return this.then(DatadogSemanticsElement(name, isImage))
}

/**
Expand All @@ -37,16 +39,7 @@ fun Modifier.datadog(name: String, isImage: Boolean = false): Modifier {
*/
internal fun Modifier.instrumentedDatadog(name: String, isImage: Boolean): Modifier {
sendTelemetry(autoInstrumented = true, InstrumentationType.Semantics)
return this.datadogSemantics(name, isImage)
}

private fun Modifier.datadogSemantics(name: String, isImage: Boolean): Modifier {
return this.semantics {
this.datadog = name
if (isImage) {
this[SemanticsProperties.Role] = Role.Image
}
}
return this.then(DatadogSemanticsElement(name, isImage))
}

internal val DatadogSemanticsPropertyKey: SemanticsPropertyKey<String> = SemanticsPropertyKey(
Expand All @@ -56,4 +49,63 @@ internal val DatadogSemanticsPropertyKey: SemanticsPropertyKey<String> = Semanti
}
)

private var SemanticsPropertyReceiver.datadog by DatadogSemanticsPropertyKey
internal var SemanticsPropertyReceiver.datadog by DatadogSemanticsPropertyKey

/**
* A custom [ModifierNodeElement] that provides Datadog semantics without participating in
* layout measurement. This replaces the previous `Modifier.semantics {}` approach to avoid
* interference with layout constraint propagation in components like `SubcomposeLayout`
* (e.g., Coil's `SubcomposeAsyncImage`) inside `LazyRow`/`LazyColumn`.
*
* By implementing only [SemanticsModifierNode] (and not `LayoutModifierNode`), this modifier
* node is explicitly excluded from the layout measurement chain, ensuring it never modifies
* constraints passed to child composables.
*/
internal class DatadogSemanticsElement(
private val name: String,
private val isImage: Boolean
) : ModifierNodeElement<DatadogSemanticsNode>() {
override fun create(): DatadogSemanticsNode = DatadogSemanticsNode(name, isImage)

override fun update(node: DatadogSemanticsNode) {
node.name = name
node.isImage = isImage
}

override fun InspectorInfo.inspectableProperties() {
this.properties["name"] = name
this.properties["isImage"] = isImage
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DatadogSemanticsElement) return false
return name == other.name && isImage == other.isImage
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + isImage.hashCode()
return result
}
}

/**
* A [SemanticsModifierNode] that attaches Datadog component metadata to the semantics tree.
* This node does NOT implement `LayoutModifierNode`, so it is never consulted during
* layout measurement and cannot modify constraints.
*/
internal class DatadogSemanticsNode(
var name: String,
var isImage: Boolean
) : Modifier.Node(), SemanticsModifierNode {
override val shouldMergeDescendantSemantics: Boolean get() = false
override val shouldClearDescendantSemantics: Boolean get() = false

override fun SemanticsPropertyReceiver.applySemantics() {
this.datadog = name
if (isImage) {
this[SemanticsProperties.Role] = Role.Image
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.compose

import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.annotation.BoolForgery
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.mockito.junit.jupiter.MockitoExtension

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
class DatadogSemanticsElementTest {

@Test
fun `M create node with correct properties W create()`(
@StringForgery name: String,
@BoolForgery isImage: Boolean
) {
// Given
val element = DatadogSemanticsElement(name, isImage)

// When
val node = element.create()

// Then
assertThat(node.name).isEqualTo(name)
assertThat(node.isImage).isEqualTo(isImage)
}

@Test
fun `M update node properties W update()`(
@StringForgery initialName: String,
@StringForgery updatedName: String,
@BoolForgery initialIsImage: Boolean
) {
// Given
val element = DatadogSemanticsElement(initialName, initialIsImage)
val node = element.create()
val updatedElement = DatadogSemanticsElement(updatedName, !initialIsImage)

// When
updatedElement.update(node)

// Then
assertThat(node.name).isEqualTo(updatedName)
assertThat(node.isImage).isEqualTo(!initialIsImage)
}

@Test
fun `M be equal W same properties`(
@StringForgery name: String,
@BoolForgery isImage: Boolean
) {
// Given
val element1 = DatadogSemanticsElement(name, isImage)
val element2 = DatadogSemanticsElement(name, isImage)

// Then
assertThat(element1).isEqualTo(element2)
assertThat(element1.hashCode()).isEqualTo(element2.hashCode())
}

@Test
fun `M not be equal W different name`(
@StringForgery name1: String,
@StringForgery name2: String,
@BoolForgery isImage: Boolean
) {
// Given
val element1 = DatadogSemanticsElement(name1, isImage)
val element2 = DatadogSemanticsElement(name2, isImage)

// Then
if (name1 != name2) {
assertThat(element1).isNotEqualTo(element2)
}
}

@Test
fun `M not be equal W different isImage`(@StringForgery name: String) {
// Given
val element1 = DatadogSemanticsElement(name, true)
val element2 = DatadogSemanticsElement(name, false)

// Then
assertThat(element1).isNotEqualTo(element2)
}

@Test
fun `M node not merge descendants W shouldMergeDescendantSemantics`(
@StringForgery name: String,
@BoolForgery isImage: Boolean
) {
// Given
val element = DatadogSemanticsElement(name, isImage)
val node = element.create()

// Then
assertThat(node.shouldMergeDescendantSemantics).isFalse()
}

@Test
fun `M node not clear descendants W shouldClearDescendantSemantics`(
@StringForgery name: String,
@BoolForgery isImage: Boolean
) {
// Given
val element = DatadogSemanticsElement(name, isImage)
val node = element.create()

// Then
assertThat(node.shouldClearDescendantSemantics).isFalse()
}
}
Loading