diff --git a/build-logic/src/main/kotlin/Dependencies.kt b/build-logic/src/main/kotlin/Dependencies.kt
index d78f0fa2..41d0a31c 100644
--- a/build-logic/src/main/kotlin/Dependencies.kt
+++ b/build-logic/src/main/kotlin/Dependencies.kt
@@ -7,7 +7,7 @@ object libs {
const val junitVintage = "5.11.0"
const val junitPlatform = "1.11.0"
- const val composeBom = "2024.08.00"
+ const val composeBom = "2024.09.00"
const val androidXTestAnnotation = "1.0.1"
const val androidXTestCore = "1.6.1"
const val androidXTestMonitor = "1.7.2"
diff --git a/build-logic/src/main/kotlin/Environment.kt b/build-logic/src/main/kotlin/Environment.kt
index 2e21c55b..5d894b23 100644
--- a/build-logic/src/main/kotlin/Environment.kt
+++ b/build-logic/src/main/kotlin/Environment.kt
@@ -97,7 +97,7 @@ object Artifacts {
*/
object Instrumentation {
const val groupId = "de.mannodermaus.junit5"
- private const val currentVersion = "1.5.1-SNAPSHOT"
+ private const val currentVersion = "1.6.0-SNAPSHOT"
private const val latestStableVersion = "1.5.0"
val Core = Deployed(
diff --git a/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml b/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml
index 5cbc61df..c9edea7d 100644
--- a/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml
+++ b/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml
@@ -10,12 +10,15 @@
- true
+ true
+ true
+ false
+ false
\ No newline at end of file
diff --git a/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml b/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml
index 639e7226..9dabb6df 100644
--- a/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml
+++ b/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml
@@ -10,11 +10,15 @@
-
+
-
+ true
+ true
+ false
+ false
+
\ No newline at end of file
diff --git a/instrumentation/CHANGELOG.md b/instrumentation/CHANGELOG.md
index 5527c406..4f1fbb12 100644
--- a/instrumentation/CHANGELOG.md
+++ b/instrumentation/CHANGELOG.md
@@ -4,6 +4,7 @@ Change Log
## Unreleased
- Use square brackets for parameterized tests to ensure that their logs show correctly in the IDE (#350)
+- Add missing API methods from JUnit 4's ComposeTestRule to JUnit 5's ComposeContext (#353)
## 1.5.0 (2024-07-25)
diff --git a/instrumentation/compose/api/compose.api b/instrumentation/compose/api/compose.api
index 2b7a0ef2..227624ab 100644
--- a/instrumentation/compose/api/compose.api
+++ b/instrumentation/compose/api/compose.api
@@ -29,6 +29,20 @@ public abstract interface class de/mannodermaus/junit5/compose/ComposeContext :
public abstract fun unregisterIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
public abstract fun waitForIdle ()V
public abstract fun waitUntil (JLkotlin/jvm/functions/Function0;)V
+ public abstract fun waitUntil (Ljava/lang/String;JLkotlin/jvm/functions/Function0;)V
+ public abstract fun waitUntilAtLeastOneExists (Landroidx/compose/ui/test/SemanticsMatcher;J)V
+ public abstract fun waitUntilDoesNotExist (Landroidx/compose/ui/test/SemanticsMatcher;J)V
+ public abstract fun waitUntilExactlyOneExists (Landroidx/compose/ui/test/SemanticsMatcher;J)V
+ public abstract fun waitUntilNodeCount (Landroidx/compose/ui/test/SemanticsMatcher;IJ)V
+}
+
+public final class de/mannodermaus/junit5/compose/ComposeContext$DefaultImpls {
+ public static synthetic fun waitUntil$default (Lde/mannodermaus/junit5/compose/ComposeContext;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
+ public static synthetic fun waitUntil$default (Lde/mannodermaus/junit5/compose/ComposeContext;Ljava/lang/String;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
+ public static synthetic fun waitUntilAtLeastOneExists$default (Lde/mannodermaus/junit5/compose/ComposeContext;Landroidx/compose/ui/test/SemanticsMatcher;JILjava/lang/Object;)V
+ public static synthetic fun waitUntilDoesNotExist$default (Lde/mannodermaus/junit5/compose/ComposeContext;Landroidx/compose/ui/test/SemanticsMatcher;JILjava/lang/Object;)V
+ public static synthetic fun waitUntilExactlyOneExists$default (Lde/mannodermaus/junit5/compose/ComposeContext;Landroidx/compose/ui/test/SemanticsMatcher;JILjava/lang/Object;)V
+ public static synthetic fun waitUntilNodeCount$default (Lde/mannodermaus/junit5/compose/ComposeContext;Landroidx/compose/ui/test/SemanticsMatcher;IJILjava/lang/Object;)V
}
public abstract interface class de/mannodermaus/junit5/compose/ComposeExtension : org/junit/jupiter/api/extension/Extension {
diff --git a/instrumentation/compose/build.gradle.kts b/instrumentation/compose/build.gradle.kts
index 79e2f57a..21b46c96 100644
--- a/instrumentation/compose/build.gradle.kts
+++ b/instrumentation/compose/build.gradle.kts
@@ -87,6 +87,10 @@ dependencies {
api(libs.composeUiTestJUnit4)
implementation(libs.composeUiTestManifest)
+ testImplementation(libs.junitJupiterApi)
+ testImplementation(libs.junitJupiterParams)
+ testRuntimeOnly(libs.junitJupiterEngine)
+
androidTestImplementation(libs.junitJupiterApi)
androidTestImplementation(libs.junitJupiterParams)
androidTestImplementation(libs.espressoCore)
diff --git a/instrumentation/compose/src/main/java/de/mannodermaus/junit5/compose/ComposeContext.kt b/instrumentation/compose/src/main/java/de/mannodermaus/junit5/compose/ComposeContext.kt
index 8e5c5289..7d8e1f66 100644
--- a/instrumentation/compose/src/main/java/de/mannodermaus/junit5/compose/ComposeContext.kt
+++ b/instrumentation/compose/src/main/java/de/mannodermaus/junit5/compose/ComposeContext.kt
@@ -1,6 +1,5 @@
package de.mannodermaus.junit5.compose
-import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
@@ -10,23 +9,41 @@ import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
+import androidx.compose.ui.test.waitUntilAtLeastOneExists
+import androidx.compose.ui.test.waitUntilDoesNotExist
+import androidx.compose.ui.test.waitUntilExactlyOneExists
+import androidx.compose.ui.test.waitUntilNodeCount
import androidx.compose.ui.unit.Density
-import androidx.test.core.app.ActivityScenario
/**
* A context through which composable blocks can be orchestrated within a [ComposeExtension].
*/
public sealed interface ComposeContext : SemanticsNodeInteractionsProvider {
- // Internal note: The below method list is a copy of `ComposeUiTest`,
+ // Internal note: The below method list is a copy of `ComposeContentTestRule`,
// preventing the viral spread of its ExperimentalTestApi annotation
- // into the consumer's codebase
+ // into the consumer's codebase and separating it from JUnit 4's TestRule
public val density: Density
public val mainClock: MainTestClock
public fun runOnUiThread(action: () -> T): T
public fun runOnIdle(action: () -> T): T
public fun waitForIdle()
public suspend fun awaitIdle()
- public fun waitUntil(timeoutMillis: Long, condition: () -> Boolean)
+ public fun waitUntil(timeoutMillis: Long = 1_000L, condition: () -> Boolean)
+ public fun waitUntil(
+ conditionDescription: String,
+ timeoutMillis: Long = 1_000L,
+ condition: () -> Boolean
+ )
+
+ public fun waitUntilNodeCount(
+ matcher: SemanticsMatcher,
+ count: Int,
+ timeoutMillis: Long = 1_000L
+ )
+
+ public fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long = 1_000L)
+ public fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long = 1_000L)
+ public fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long = 1_000L)
public fun registerIdlingResource(idlingResource: IdlingResource)
public fun unregisterIdlingResource(idlingResource: IdlingResource)
public fun setContent(composable: @Composable () -> Unit)
@@ -37,52 +54,56 @@ internal class ComposeContextImpl(
private val delegate: ComposeUiTest
) : ComposeContext, SemanticsNodeInteractionsProvider by delegate {
- override fun onAllNodes(
- matcher: SemanticsMatcher,
- useUnmergedTree: Boolean
- ): SemanticsNodeInteractionCollection {
- return delegate.onAllNodes(matcher, useUnmergedTree)
- }
-
- override fun onNode(
- matcher: SemanticsMatcher,
- useUnmergedTree: Boolean
- ): SemanticsNodeInteraction {
- return delegate.onNode(matcher, useUnmergedTree)
- }
-
override val density: Density get() = delegate.density
+
override val mainClock: MainTestClock get() = delegate.mainClock
- override fun runOnUiThread(action: () -> T): T {
- return delegate.runOnUiThread(action)
- }
+ override fun runOnUiThread(action: () -> T): T = delegate.runOnUiThread(action)
- override fun runOnIdle(action: () -> T): T {
- return delegate.runOnIdle(action)
- }
+ override fun runOnIdle(action: () -> T): T = delegate.runOnIdle(action)
- override fun waitForIdle() {
- delegate.waitForIdle()
- }
+ override fun waitForIdle() = delegate.waitForIdle()
- override suspend fun awaitIdle() {
- delegate.awaitIdle()
- }
+ override suspend fun awaitIdle() = delegate.awaitIdle()
+
+ override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =
+ delegate.waitUntil(conditionDescription = null, timeoutMillis, condition)
- override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
- delegate.waitUntil(timeoutMillis, condition)
+ override fun waitUntil(
+ conditionDescription: String,
+ timeoutMillis: Long,
+ condition: () -> Boolean
+ ) {
+ delegate.waitUntil(conditionDescription, timeoutMillis, condition)
}
- override fun registerIdlingResource(idlingResource: IdlingResource) {
+ override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =
+ delegate.waitUntilNodeCount(matcher, count, timeoutMillis)
+
+ override fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
+ delegate.waitUntilAtLeastOneExists(matcher, timeoutMillis)
+
+ override fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
+ delegate.waitUntilExactlyOneExists(matcher, timeoutMillis)
+
+ override fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long) =
+ delegate.waitUntilDoesNotExist(matcher, timeoutMillis)
+
+ override fun registerIdlingResource(idlingResource: IdlingResource) =
delegate.registerIdlingResource(idlingResource)
- }
- override fun unregisterIdlingResource(idlingResource: IdlingResource) {
+ override fun unregisterIdlingResource(idlingResource: IdlingResource) =
delegate.unregisterIdlingResource(idlingResource)
- }
- override fun setContent(composable: @Composable () -> Unit) {
- delegate.setContent(composable)
- }
+ override fun onNode(
+ matcher: SemanticsMatcher,
+ useUnmergedTree: Boolean
+ ): SemanticsNodeInteraction = delegate.onNode(matcher, useUnmergedTree)
+
+ override fun onAllNodes(
+ matcher: SemanticsMatcher,
+ useUnmergedTree: Boolean
+ ): SemanticsNodeInteractionCollection = delegate.onAllNodes(matcher, useUnmergedTree)
+
+ override fun setContent(composable: @Composable () -> Unit) = delegate.setContent(composable)
}
diff --git a/instrumentation/compose/src/test/java/de/mannodermaus/junit5/compose/ComposeContextTests.kt b/instrumentation/compose/src/test/java/de/mannodermaus/junit5/compose/ComposeContextTests.kt
new file mode 100644
index 00000000..eed1072f
--- /dev/null
+++ b/instrumentation/compose/src/test/java/de/mannodermaus/junit5/compose/ComposeContextTests.kt
@@ -0,0 +1,31 @@
+package de.mannodermaus.junit5.compose
+
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import org.junit.jupiter.api.Assertions.fail
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import java.lang.reflect.Method
+
+class ComposeContextTests {
+ companion object {
+ @JvmStatic
+ fun relevantMethods() = buildList {
+ addAll(ComposeTestRule::class.java.relevantMethods)
+ addAll(ComposeContentTestRule::class.java.relevantMethods)
+ }
+
+ private val Class.relevantMethods
+ get() = declaredMethods.filter { '$' !in it.name }
+ }
+
+ @MethodSource("relevantMethods")
+ @ParameterizedTest(name = "ComposeContext defines {0} correctly")
+ fun test(method: Method) {
+ try {
+ ComposeContext::class.java.getDeclaredMethod(method.name, *method.parameterTypes)
+ } catch (ignored: NoSuchMethodException) {
+ fail("ComposeContext does not define method $method")
+ }
+ }
+}