diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fd12bed90..dc74fdae405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Fixes + +- (Jetpack Compose) Modifier.sentryTag now uses Modifier.Node ([#4029](https://github.com/getsentry/sentry-java/pull/4029)) + - This allows Composables that use this modifier to be skippable + ## 7.21.0 ### Fixes diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index bf5dba03be5..8f7f495e27f 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -201,6 +201,7 @@ object Config { val javaFaker = "com.github.javafaker:javafaker:1.0.2" val msgpack = "org.msgpack:msgpack-core:0.9.8" val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14" + val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:$composeVersion" } object QualityPlugins { diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 114c08a22ff..a31027a5a3a 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -60,6 +60,11 @@ kotlin { implementation(Config.TestLibs.mockitoKotlin) implementation(Config.TestLibs.mockitoInline) implementation(Config.Libs.composeNavigation) + implementation(Config.TestLibs.robolectric) + implementation(Config.TestLibs.androidxRunner) + implementation(Config.TestLibs.androidxJunit) + implementation(Config.TestLibs.androidxTestRules) + implementation(Config.TestLibs.composeUiTestJunit4) } } } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt index f1f43c9c8bb..39ac3216610 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt @@ -1,8 +1,13 @@ package io.sentry.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.SemanticsConfiguration +import androidx.compose.ui.semantics.SemanticsModifier import androidx.compose.ui.semantics.SemanticsPropertyKey -import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.SemanticsPropertyReceiver public object SentryModifier { @@ -19,11 +24,35 @@ public object SentryModifier { ) @JvmStatic - public fun Modifier.sentryTag(tag: String): Modifier { - return semantics( - properties = { - this[SentryTag] = tag + public fun Modifier.sentryTag(tag: String): Modifier = + this then SentryTagModifierNodeElement(tag) + + private data class SentryTagModifierNodeElement(val tag: String) : + ModifierNodeElement(), SemanticsModifier { + + override val semanticsConfiguration: SemanticsConfiguration = + SemanticsConfiguration().also { + it[SentryTag] = tag } - ) + + override fun create(): SentryTagModifierNode = SentryTagModifierNode(tag) + + override fun update(node: SentryTagModifierNode) { + node.tag = tag + } + + override fun InspectorInfo.inspectableProperties() { + name = "sentryTag" + properties["tag"] = tag + } + } + + private class SentryTagModifierNode(var tag: String) : + Modifier.Node(), + SemanticsModifierNode { + + override fun SemanticsPropertyReceiver.applySemantics() { + this[SentryTag] = tag + } } } diff --git a/sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt b/sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt new file mode 100644 index 00000000000..38aa2585d3f --- /dev/null +++ b/sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt @@ -0,0 +1,59 @@ +package io.sentry.compose + +import android.app.Application +import android.content.ComponentName +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.compose.SentryModifier.sentryTag +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.robolectric.Shadows +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [30]) +class SentryModifierComposeTest { + + companion object { + private const val TAG_VALUE = "ExampleTagValue" + } + + // workaround for robolectric tests with composeRule + // from https://github.com/robolectric/robolectric/pull/4736#issuecomment-1831034882 + @get:Rule(order = 1) + val addActivityToRobolectricRule = object : TestWatcher() { + override fun starting(description: Description?) { + super.starting(description) + val appContext: Application = ApplicationProvider.getApplicationContext() + Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent( + ComponentName( + appContext.packageName, + ComponentActivity::class.java.name + ) + ) + } + } + + @get:Rule(order = 2) + val rule = createComposeRule() + + @Test + fun sentryModifierAppliesTag() { + rule.setContent { + Box(modifier = Modifier.sentryTag(TAG_VALUE)) + } + rule.onNode( + SemanticsMatcher(TAG_VALUE) { + it.config.find { (key, _) -> key.name == SentryModifier.TAG }?.value == TAG_VALUE + } + ).assertExists() + } +}